diff --git a/ChangeLog b/ChangeLog
index 3b9f77bab3..60e5480324 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -11,6 +11,8 @@
(#721, #458)
### Changed
+- Command-line tools are now implemented in `pygambit` rather than C++-based compiled programs.
+ The `gambit-convert` utility has been removed.
- Refreshed GUI icons and standardised dialog layout and formatting
- Improved installers for Windows, and disk-image "installer" for MacOS
- Revised `StrategySupportProfile` to expose a slimmer set of operations, and
diff --git a/Makefile.am b/Makefile.am
index 797b44e623..0e0eb8f556 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -25,6 +25,7 @@ ACLOCAL_AMFLAGS = -I m4
include build_support/catalog/catalog.am
EXTRA_DIST = \
+ build_support/GAMBIT_VERSION \
build_support/msw/gambit.wxs.in \
build_support/msw/banner.bmp \
build_support/msw/dialog.bmp \
@@ -337,27 +338,14 @@ enumpoly_SOURCES = \
src/solvers/enumpoly/nfgpoly.cc \
src/solvers/enumpoly/enumpoly.h
-noinst_HEADERS = src/gambit.h src/solvers.h src/solvers/nash.h
+noinst_HEADERS = src/gambit.h src/solvers.h src/solvers/nash.h src/solvers/enumpure/enumpure.h
if IS_WIN32
AM_LDFLAGS = -static -static-libgcc -static-libstdc++
endif
-bin_PROGRAMS = \
- gambit-convert \
- gambit-enummixed \
- gambit-enumpoly \
- gambit-enumpure \
- gambit-gnm \
- gambit-ipa \
- gambit-lcp \
- gambit-liap \
- gambit-logit \
- gambit-lp \
- gambit-simpdiv
-
if WITH_GUI
-bin_PROGRAMS += gambit
+bin_PROGRAMS = gambit
endif
# Read version from GAMBIT_VERSION file for use in compilation
@@ -385,64 +373,6 @@ liblogit_a_SOURCES = ${logit_SOURCES}
libsimpdiv_a_SOURCES = ${simpdiv_SOURCES}
libenumpoly_a_SOURCES = ${enumpoly_SOURCES}
-## Command-line tools
-
-gambit_convert_SOURCES = \
- src/tools/convert/convert.cc
-gambit_convert_LDADD = libgames.a libcore.a
-
-gambit_enummixed_SOURCES = \
- src/tools/util.h \
- src/tools/enummixed/enummixed.cc
-gambit_enummixed_LDADD = libbimatrix.a libgames.a libcore.a
-
-gambit_enumpoly_SOURCES = \
- src/tools/util.h \
- src/tools/enumpoly/enumpoly.cc
-gambit_enumpoly_LDADD = libenumpoly.a libgames.a libcore.a
-
-gambit_enumpure_SOURCES = \
- src/solvers/enumpure/enumpure.h \
- src/tools/util.h \
- src/tools/enumpure/enumpure.cc
-gambit_enumpure_LDADD = libgames.a libcore.a
-
-gambit_gnm_SOURCES = \
- src/tools/gt/nfggt.cc \
- src/tools/gt/nfggnm.cc
-gambit_gnm_LDADD = libgtracer.a libgames.a libcore.a
-
-gambit_ipa_SOURCES = \
- src/tools/gt/nfggt.cc \
- src/tools/util.h \
- src/tools/gt/nfgipa.cc
-gambit_ipa_LDADD = libgtracer.a libgames.a libcore.a
-
-gambit_lcp_SOURCES = \
- src/tools/util.h \
- src/tools/lcp/lcp.cc
-gambit_lcp_LDADD = libbimatrix.a libgames.a libcore.a
-
-gambit_liap_SOURCES = \
- src/tools/util.h \
- src/tools/liap/liap.cc
-gambit_liap_LDADD = libliap.a libgames.a libcore.a
-
-gambit_logit_SOURCES = \
- src/tools/util.h \
- src/tools/logit/logit.cc
-gambit_logit_LDADD = liblogit.a libgames.a libcore.a
-
-gambit_lp_SOURCES = \
- src/tools/util.h \
- src/tools/lp/lp.cc
-gambit_lp_LDADD = libbimatrix.a libgames.a libcore.a
-
-gambit_simpdiv_SOURCES = \
- src/tools/util.h \
- src/tools/simpdiv/nfgsimpdiv.cc
-gambit_simpdiv_LDADD = libsimpdiv.a libgames.a libcore.a
-
gambit_SOURCES = \
src/gui/analysis.cc \
src/gui/analysis.h \
@@ -554,4 +484,3 @@ clang-tidy:
clang-tidy ${top_srcdir}/src/core/*.cc -- --std=c++20 -I ${top_srcdir}/src -DVERSION=\"$(GAMBIT_VERSION)\"
clang-tidy ${top_srcdir}/src/games/*.cc ${top_srcdir}/src/games/*/*.cc -- --std=c++20 -I ${top_srcdir}/src -DVERSION=\"$(GAMBIT_VERSION)\"
clang-tidy ${top_srcdir}/src/solvers/*/*.cc -- --std=c++20 -I ${top_srcdir}/src -DVERSION=\"$(GAMBIT_VERSION)\"
- clang-tidy ${top_srcdir}/src/tools/*/*.cc -- --std=c++20 -I ${top_srcdir}/src -DVERSION=\"$(GAMBIT_VERSION)\"
diff --git a/build_support/msw/gambit.wxs.in b/build_support/msw/gambit.wxs.in
index a891d4ada2..f43976bd56 100644
--- a/build_support/msw/gambit.wxs.in
+++ b/build_support/msw/gambit.wxs.in
@@ -79,33 +79,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -130,15 +103,6 @@
-
-
-
-
-
-
-
-
-
diff --git a/doc/index.rst b/doc/index.rst
index c4fa8119dc..ad354146a0 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -69,7 +69,7 @@ construction and analysis of finite extensive and strategic games.
.. grid-item-card:: 💻 Command-line interface
:columns: 3
- Use Gambit's command-line tools for scripting.
+ Run equilibrium calculations from the command-line.
.. button-ref:: command-line
:ref-type: ref
diff --git a/doc/install.rst b/doc/install.rst
index c0a1eac9c0..44db4e342e 100644
--- a/doc/install.rst
+++ b/doc/install.rst
@@ -100,12 +100,6 @@ The current stable release of Gambit is |release|.
`./configure --prefix=/your/path/here`
- .. warning::
- The graphical interface relies on external calls to other
- programs built in this process, especially for the computation of
- equilibria. It is strongly recommended that you install the Gambit
- executables to a directory in your path!
-
All past releases are available on the
`GitHub releases page `__.
diff --git a/doc/pygambit.rst b/doc/pygambit.rst
index ce251cb5bf..1c8f0f7723 100644
--- a/doc/pygambit.rst
+++ b/doc/pygambit.rst
@@ -6,12 +6,14 @@ PyGambit
See installation instructions in the :ref:`install` section.
-
For newcomers to Gambit, we recommend reading through the PyGambit tutorials, which demonstrate the API's key capabilities for analyzing and solving games.
All of these tutorials assume a basic knowledge of programming in Python.
You can run the tutorials interactively as Jupyter notebooks, see :ref:`local_tutorials`.
+Installing PyGambit also installs Gambit's :ref:`command-line tools `
+for computing Nash equilibria from the shell or from scripts.
+
New user tutorials
------------------
diff --git a/doc/tools.rst b/doc/tools.rst
index 7f5aba15a2..2efab1a955 100644
--- a/doc/tools.rst
+++ b/doc/tools.rst
@@ -4,17 +4,21 @@
CLI
***
-See installation instructions in the :ref:`install-cli-gui` section.
-
Gambit provides command-line interfaces for each method for computing
Nash equilibria. These are suitable for scripting or calling from
other programs. This chapter describes the use of these programs.
For a general overview of methods for computing equilibria,
see the survey of :cite:p:`McKMcL96`.
-The graphical interface also provides a frontend for calling these
-programs and evaluating their output. Direct use of the command-line
-programs is intended for advanced users and applications.
+.. versionchanged:: 17.0.0
+
+ These programs are now implemented in Python, as part of the
+ :ref:`pygambit ` package, rather than being separate
+ C++ executables. Installing PyGambit (``pip install pygambit``,
+ see :ref:`install`) installs these command-line programs alongside
+ it; no separate download or build is needed. Each program's
+ command-line interface, options, and output are unchanged from
+ the previous C++ versions, except as otherwise noted.
These programs take an extensive or strategic game file, which can be
specified on the command line or piped via standard input, and output
@@ -49,4 +53,3 @@ documentation.
tools.logit
tools.gnm
tools.ipa
- tools.convert
diff --git a/pyproject.toml b/pyproject.toml
index e2e4e55f26..9f5ebbff88 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,8 +29,21 @@ dependencies = [
"numpy",
"scipy",
"pandas",
+ "click",
]
+[project.scripts]
+gambit-enumpure = "pygambit.cli.enumpure:main"
+gambit-logit = "pygambit.cli.logit:main"
+gambit-enummixed = "pygambit.cli.enummixed:main"
+gambit-lcp = "pygambit.cli.lcp:main"
+gambit-lp = "pygambit.cli.lp:main"
+gambit-liap = "pygambit.cli.liap:main"
+gambit-simpdiv = "pygambit.cli.simpdiv:main"
+gambit-gnm = "pygambit.cli.gnm:main"
+gambit-ipa = "pygambit.cli.ipa:main"
+gambit-enumpoly = "pygambit.cli.enumpoly:main"
+
[project.optional-dependencies]
test = ["pytest", "pytest-subtests", "nbformat", "nbclient", "ipykernel"]
doc = [
@@ -124,7 +137,7 @@ markers = [
]
[tool.setuptools]
-packages = ["pygambit"]
+packages = ["pygambit", "pygambit.cli"]
package-dir = { "pygambit" = "src/pygambit" }
[tool.setuptools.package-data]
diff --git a/src/pygambit/cli/__init__.py b/src/pygambit/cli/__init__.py
new file mode 100644
index 0000000000..f25d3612a0
--- /dev/null
+++ b/src/pygambit/cli/__init__.py
@@ -0,0 +1,22 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/__init__.py
+# Command-line tools for computing Nash equilibria, implemented in Python
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line tools for computing Nash equilibria, implemented in Python."""
diff --git a/src/pygambit/cli/common.py b/src/pygambit/cli/common.py
new file mode 100644
index 0000000000..e603f0a9d7
--- /dev/null
+++ b/src/pygambit/cli/common.py
@@ -0,0 +1,374 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/common.py
+# Infrastructure shared by Gambit's command-line tools
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Infrastructure shared by Gambit's command-line tools."""
+
+from __future__ import annotations
+
+import functools
+import io
+import pathlib
+import sys
+
+import click
+
+import pygambit as gbt
+
+_GAME_FORMATS = (
+ ("NFG", gbt.read_nfg),
+ ("EFG", gbt.read_efg),
+ ("#AGG", gbt.read_agg),
+ ("#BAGG", gbt.read_bagg),
+)
+
+
+def _gambit_version() -> str:
+ """The Gambit version string, in the same (not necessarily PEP 440-normalized)
+ form as the C++ tools' ``VERSION`` macro, which is taken verbatim from
+ `build_support/GAMBIT_VERSION`.
+ """
+ version_file = pathlib.Path(__file__).parents[3] / "build_support/GAMBIT_VERSION"
+ if version_file.exists():
+ return version_file.read_text().strip()
+ return gbt.__version__
+
+
+def open_game_file(path: str | None, prog_name: str) -> io.IOBase:
+ """Open `path` for reading, or return standard input if `path` is `None` or
+ `"-"`. A failure to open the file is reported the way the C++ tools' call to
+ `perror()` does, and exits with status 1.
+ """
+ if path is None or path == "-":
+ return sys.stdin.buffer
+ try:
+ return open(path, "rb")
+ except OSError as exc:
+ click.echo(f"{prog_name}: {path}: {exc.strerror}", err=True)
+ sys.exit(1)
+
+
+def read_game(source: io.IOBase) -> gbt.Game:
+ """Read a game from an open file (or standard input), auto-detecting its file
+ format the way the underlying C++ tools' ``ReadGame()`` does: first as a
+ (legacy) ``.gbt`` workspace, and, failing that, by the leading token of the
+ file, which identifies it as an NFG, EFG, AGG, or BAGG representation.
+ """
+ data = source.read()
+ if isinstance(data, str):
+ data = data.encode("utf-8")
+ if not data.strip():
+ raise ValueError("Empty file or string provided")
+ try:
+ return gbt.read_gbt(io.BytesIO(data))
+ except Exception:
+ pass
+ text = data.decode("utf-8", errors="replace").lstrip()
+ for tag, reader in _GAME_FORMATS:
+ if text.startswith(tag):
+ try:
+ return reader(io.BytesIO(data))
+ except Exception as exc:
+ raise ValueError(f"Parse error in game file: {exc}") from None
+ raise ValueError("Unrecognized file format")
+
+
+def print_banner(description: str, extra_lines: tuple[str, ...] = ()) -> None:
+ """Print the standard Gambit command-line tool banner to standard error. Some
+ tools (`gambit-gnm`, `gambit-ipa`) insert additional lines, such as a Gametracer
+ credit, between the description and the Gambit version line; pass those as
+ `extra_lines`.
+ """
+ click.echo(description, err=True)
+ for line in extra_lines:
+ click.echo(line, err=True)
+ click.echo(
+ f"Gambit version {_gambit_version()}, Copyright (C) 1994-2026, The Gambit Project",
+ err=True,
+ )
+ click.echo("This is free software, distributed under the GNU GPL", err=True)
+ click.echo(err=True)
+
+
+def version_option(description: str, extra_lines: tuple[str, ...] = ()) -> callable:
+ """A ``-v``/``--version`` option which prints the tool's banner and exits,
+ matching the behavior of the C++ command-line tools. See `print_banner` for
+ the meaning of `extra_lines`.
+ """
+
+ def callback(ctx: click.Context, _param: click.Parameter, value: bool) -> None:
+ if not value or ctx.resilient_parsing:
+ return
+ print_banner(description, extra_lines)
+ ctx.exit(0)
+
+ return click.option(
+ "-v",
+ "--version",
+ is_flag=True,
+ expose_value=False,
+ is_eager=True,
+ callback=callback,
+ help="print version information",
+ )
+
+
+def handle_errors(f: callable) -> callable:
+ """Decorator which reports errors the way the C++ command-line tools do:
+ a message on standard error and exit code 1, instead of a Python traceback.
+ """
+
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ try:
+ f(*args, **kwargs)
+ except (ValueError, RuntimeError, OSError) as exc:
+ click.echo(f"Error: {exc}", err=True)
+ sys.exit(1)
+
+ return wrapper
+
+
+def validate_stop_after(
+ ctx: click.Context, param: click.Parameter, value: str | None
+) -> int | None:
+ """A click callback for a ``-e``/``stop_after``-style option: validates that `value`
+ is a positive integer, matching the C++ tools' `std::from_chars`-based validation,
+ reporting the same error text and exiting immediately (before the tool's banner)
+ on failure.
+ """
+ if value is None:
+ return None
+ if not value.isdigit() or int(value) == 0:
+ click.echo(f"Error: -e argument must be a positive integer; got '{value}'.", err=True)
+ sys.exit(1)
+ return int(value)
+
+
+def format_value(
+ value: gbt.Rational | float,
+ decimals: int,
+ fixed: bool = True,
+ as_float: bool = False,
+) -> str:
+ """Format a probability or payoff value the way the C++ tools do: exactly, for a
+ `Rational`, or, for a float, fixed-point with `decimals` digits after the point
+ (`fixed=True`, matching `MixedStrategyProfileCSVRenderer`/`MixedBehaviorProfileCSVRenderer`),
+ or with `decimals` significant digits, trimmed of trailing zeroes and switching to
+ scientific notation as needed (`fixed=False`, matching `gambit-logit`'s own
+ `PrintProfile`, which explicitly unsets `std::ios::fixed` before printing profile
+ values). If `as_float` is True, a `Rational` is first converted to a float and
+ formatted the same way as a float would be, matching `gambit-simpdiv`'s
+ `MixedStrategyCSVAsFloatRenderer`.
+ """
+ if isinstance(value, gbt.Rational) and not as_float:
+ return str(value)
+ value = float(value)
+ return f"{value:.{decimals}f}" if fixed else f"{value:.{decimals}g}"
+
+
+def _is_behavior_profile(profile) -> bool:
+ return "Behavior" in type(profile).__name__
+
+
+def render_profile_csv(
+ profile: gbt.MixedStrategyProfile | gbt.MixedBehaviorProfile,
+ label: str,
+ decimals: int,
+ fixed: bool = True,
+ as_float: bool = False,
+) -> str:
+ """Render a strategy or behavior profile as a single comma-separated line,
+ tagged with `label`, matching `MixedStrategyProfileCSVRenderer` and
+ `MixedBehaviorProfileCSVRenderer` in the C++ tools. See `format_value` for
+ the meaning of `fixed` and `as_float`.
+ """
+ if _is_behavior_profile(profile):
+ values = [
+ prob
+ for player in profile.game.players
+ for _infoset, action in profile[player.label]
+ for _label, prob in action
+ ]
+ else:
+ values = [
+ prob for player in profile.game.players for _label, prob in profile[player.label]
+ ]
+ return ",".join([label, *(format_value(v, decimals, fixed, as_float) for v in values)])
+
+
+def render_support_csv(
+ support: gbt.StrategySupportProfile | gbt.BehaviorSupportProfile,
+ label: str,
+) -> str:
+ """Render a strategy or behavior support profile as a single comma-separated line,
+ tagged with `label`: one field per player (strategy support) or per information set
+ (behavior support), each field a string of `1`/`0` digits marking membership in the
+ support, matching `gambit-enumpoly`'s own `PrintSupport`.
+ """
+ if isinstance(support, gbt.BehaviorSupportProfile):
+ fields = [
+ "".join(
+ "1" if action.label in action_support else "0"
+ for action in action_support.infoset.actions
+ )
+ for player in support.game.players
+ for action_support in support[player.label]
+ ]
+ else:
+ fields = [
+ "".join(
+ "1" if strategy.label in support[player.label] else "0"
+ for strategy in player.strategies
+ )
+ for player in support.game.players
+ ]
+ return ",".join([label, *fields])
+
+
+def render_profile_detail(
+ profile: gbt.MixedStrategyProfile | gbt.MixedBehaviorProfile,
+ decimals: int,
+) -> str:
+ """Render a strategy or behavior profile as a human-readable description,
+ matching `MixedStrategyProfileDetailRenderer` and
+ `MixedBehaviorProfileDetailRenderer` in the C++ tools.
+ """
+ if _is_behavior_profile(profile):
+ return _render_behavior_detail(profile, decimals)
+ return _render_strategy_detail(profile, decimals)
+
+
+def _name_or_number(obj) -> str:
+ # Gambit's Python API numbers players/strategies/infosets/actions from 0;
+ # the C++ tools display the underlying (1-based) engine numbering.
+ return obj.label if obj.label else str(obj.number + 1)
+
+
+def _render_strategy_detail(profile: gbt.MixedStrategyProfile, decimals: int) -> str:
+ lines = []
+ for player in profile.game.players:
+ lines.append(f"Strategy profile for player {player.number + 1}:")
+ lines.append("Strategy Prob Value")
+ lines.append("-------- ----------- -----------")
+ probs = profile[player.label]
+ values = profile.strategy_values[player.label]
+ for strategy in player.strategies:
+ name = _name_or_number(strategy)
+ prob = format_value(probs[strategy.label], decimals)
+ value = format_value(values[strategy.label], decimals)
+ lines.append(f"{name:>8} {prob:>10} {value:>11}")
+ return "\n".join(lines)
+
+
+def _render_behavior_detail(profile: gbt.MixedBehaviorProfile, decimals: int) -> str:
+ lines = []
+ action_values = profile.action_values
+ beliefs = profile.beliefs
+ realiz_probs = profile.realiz_probs
+ for player in profile.game.players:
+ lines.append(f"Behavior profile for player {player.number + 1}:")
+ lines.append("Infoset Action Prob Value")
+ lines.append("------- ------- ----------- -----------")
+ for infoset, mixed_action in profile[player.label]:
+ infoset_name = _name_or_number(infoset)
+ values = action_values[next(iter(infoset.members))]
+ for action in infoset.actions:
+ prob = mixed_action[action.label]
+ value = values[action.label]
+ value_text = format_value(value, decimals) if value is not None else ""
+ lines.append(
+ f"{infoset_name:>7} {_name_or_number(action):>7} "
+ f"{format_value(prob, decimals):>11} {value_text:>11}"
+ )
+ lines.append("")
+ lines.append("Infoset Node Belief Prob")
+ lines.append("------- ------- ----------- -----------")
+ for infoset, _mixed_action in profile[player.label]:
+ infoset_name = _name_or_number(infoset)
+ for node in infoset.members:
+ node_name = _name_or_number(node)
+ belief = beliefs[node]
+ belief_text = format_value(belief, decimals) if belief is not None else ""
+ realiz_text = format_value(realiz_probs[node], decimals)
+ lines.append(
+ f"{infoset_name:>7} {node_name:>7} {belief_text:>11} {realiz_text:>11}"
+ )
+ lines.append("")
+ return "\n".join(lines)
+
+
+def read_strategy_profiles_csv(
+ path: str,
+ game: gbt.Game,
+) -> list[gbt.MixedStrategyProfileRational]:
+ """Read one mixed strategy profile per line from `path`, each a flat comma-separated
+ list of probabilities in the same order as a profile's CSV row (excluding any
+ leading label), matching the C++ tools' `ReadStrategyProfiles`/`ReadProfiles`.
+ Values are parsed as exact rationals; a method which requires floating-point
+ starting points converts the result via `~MixedStrategyProfile.as_float`.
+ """
+ strategies = [strategy for player in game.players for strategy in player.strategies]
+ profiles = []
+ for line in pathlib.Path(path).read_text().splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ fields = line.split(",")
+ try:
+ values = iter([gbt.Rational(fields[i]) for i in range(len(strategies))])
+ except (ValueError, IndexError) as exc:
+ raise ValueError(f"Error reading strategy profile from '{path}': {exc}") from None
+ profile = game.mixed_strategy_profile(rational=True)
+ for player in game.players:
+ profile[player.label] = {s.label: next(values) for s in player.strategies}
+ profiles.append(profile)
+ return profiles
+
+
+def read_behavior_profiles_csv(
+ path: str, game: gbt.Game
+) -> list[gbt.MixedBehaviorProfileRational]:
+ """Read one mixed behavior profile per line from `path`, each a flat comma-separated
+ list of probabilities in the same order as a profile's CSV row (excluding any
+ leading label), matching the C++ tools' `ReadBehaviorProfiles`. Values are parsed
+ as exact rationals; a method which requires floating-point starting points converts
+ the result via `~MixedBehaviorProfile.as_float`.
+ """
+ count = sum(
+ len(list(infoset.actions)) for player in game.players for infoset in player.infosets
+ )
+ profiles = []
+ for line in pathlib.Path(path).read_text().splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ fields = line.split(",")
+ try:
+ values = iter([gbt.Rational(fields[i]) for i in range(count)])
+ except (ValueError, IndexError) as exc:
+ raise ValueError(f"Error reading behavior profile from '{path}': {exc}") from None
+ profile = game.mixed_behavior_profile(rational=True)
+ for player in game.players:
+ for infoset in player.infosets:
+ node = next(iter(infoset.members))
+ profile[node] = {a.label: next(values) for a in infoset.actions}
+ profiles.append(profile)
+ return profiles
diff --git a/src/pygambit/cli/enummixed.py b/src/pygambit/cli/enummixed.py
new file mode 100644
index 0000000000..96039fdecb
--- /dev/null
+++ b/src/pygambit/cli/enummixed.py
@@ -0,0 +1,86 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/enummixed.py
+# Command-line driver program for mixed-strategy Nash equilibrium enumeration
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for mixed-strategy Nash equilibrium enumeration."""
+
+from __future__ import annotations
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria by enumerating extreme points"
+PROG_NAME = "gambit-enummixed"
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, reports all Nash equilibria found."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ type=int,
+ default=None,
+ help="compute using floating-point arithmetic; display results with DECIMALS digits",
+)
+@click.option("-c", "--cliques", is_flag=True, help="output connectedness information")
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@version_option(DESCRIPTION)
+@handle_errors
+def main(file: str | None, decimals: int | None, cliques: bool, quiet: bool) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+ rational = decimals is None
+
+ def render(profile) -> None:
+ click.echo(render_profile_csv(profile, "NE", decimals or 0))
+
+ result = gbt.nash.enummixed_solve(
+ game,
+ rational=rational,
+ nash_callback=render,
+ cliques=cliques,
+ )
+ if cliques:
+ for index, clique in enumerate(result.parameters["cliques"], start=1):
+ label = f"convex-{index}"
+ for profile in clique:
+ click.echo(render_profile_csv(profile, label, decimals or 0))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/enumpoly.py b/src/pygambit/cli/enumpoly.py
new file mode 100644
index 0000000000..42b0ce7901
--- /dev/null
+++ b/src/pygambit/cli/enumpoly.py
@@ -0,0 +1,147 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/enumpoly.py
+# Command-line driver program for Nash equilibrium enumeration via support
+# enumeration and polynomial system solving
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium enumeration via support
+enumeration and polynomial system solving.
+
+.. note::
+
+ Solving via PHCpack (the C++ tool's ``phcpack_path`` equivalent has no CLI
+ option) is not available here; see :func:`pygambit.nash.enumpoly_solve`'s
+ `phcpack_path` parameter for the equivalent computation from Python.
+"""
+
+from __future__ import annotations
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ render_support_csv,
+ validate_stop_after,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria by solving polynomial systems"
+PROG_NAME = "gambit-enumpoly"
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, reports all Nash equilibria found."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ default=6,
+ show_default=True,
+ type=int,
+ help="show equilibrium probabilities with DECIMALS digits",
+)
+@click.option("-S", "--strategic", is_flag=True, help="use strategic game")
+@click.option(
+ "-m",
+ "maxregret",
+ default=1.0e-8,
+ show_default=True,
+ type=float,
+ help="maximum regret acceptable as a proportion of the range of payoffs in the game",
+)
+@click.option(
+ "-e",
+ "stop_after",
+ type=str,
+ default=None,
+ callback=validate_stop_after,
+ help="terminate after finding EQA equilibria (default is to search in all supports)",
+)
+@click.option(
+ "-r",
+ "max_rectangles",
+ default=20_000,
+ show_default=True,
+ type=int,
+ help="maximum number of rectangles to examine when searching for roots on a "
+ "single support, before giving up on that support",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@click.option(
+ "-V",
+ "--verbose",
+ is_flag=True,
+ help="verbose mode (shows supports investigated); default is only to show equilibria",
+)
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int,
+ strategic: bool,
+ maxregret: float,
+ stop_after: int | None,
+ max_rectangles: int,
+ quiet: bool,
+ verbose: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+ if not game.is_perfect_recall:
+ raise ValueError("Computing equilibria of games with imperfect recall is not supported.")
+
+ def render(profile) -> None:
+ click.echo(render_profile_csv(profile, "NE", decimals))
+
+ def render_event(event) -> None:
+ if not verbose:
+ return
+ if isinstance(event, gbt.EnumPolyCandidateSupportEvent):
+ click.echo(render_support_csv(event.support, "candidate"))
+ elif isinstance(event, gbt.EnumPolySingularSupportEvent):
+ click.echo(render_support_csv(event.support, "singular"))
+ elif isinstance(event, gbt.EnumPolyBudgetExceededSupportEvent):
+ click.echo(render_support_csv(event.support, "budget-exceeded"))
+
+ gbt.nash.enumpoly_solve(
+ game,
+ use_strategic=strategic,
+ stop_after=stop_after,
+ maxregret=maxregret,
+ max_rectangles=max_rectangles,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/enumpure.py b/src/pygambit/cli/enumpure.py
new file mode 100644
index 0000000000..4b7fd7a34c
--- /dev/null
+++ b/src/pygambit/cli/enumpure.py
@@ -0,0 +1,87 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/enumpure.py
+# Command-line driver program for pure-strategy Nash equilibrium enumeration
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for pure-strategy Nash equilibrium enumeration."""
+
+from __future__ import annotations
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ render_profile_detail,
+ version_option,
+)
+
+DESCRIPTION = "Search for Nash equilibria in pure strategies"
+PROG_NAME = "gambit-enumpure"
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, locates all Nash equilibria in pure strategies."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-S",
+ "--strategic",
+ is_flag=True,
+ help="report equilibria in strategies even for extensive games",
+)
+@click.option("-A", "--agent", is_flag=True, help="compute agent form equilibria")
+@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria")
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@version_option(DESCRIPTION)
+@handle_errors
+def main(file: str | None, strategic: bool, agent: bool, detail: bool, quiet: bool) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+
+ def render(profile) -> None:
+ is_behavior = hasattr(profile, "as_strategy")
+ if strategic and is_behavior:
+ profile = profile.as_strategy()
+ elif not strategic and game.is_tree and not is_behavior:
+ profile = profile.as_behavior()
+ if detail:
+ click.echo(render_profile_detail(profile, 0))
+ else:
+ click.echo(render_profile_csv(profile, "NE", 0))
+
+ if agent and game.is_tree:
+ gbt.nash.enumpure_agent_solve(game, nash_callback=render)
+ else:
+ gbt.nash.enumpure_solve(game, nash_callback=render)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/gnm.py b/src/pygambit/cli/gnm.py
new file mode 100644
index 0000000000..1100c8b999
--- /dev/null
+++ b/src/pygambit/cli/gnm.py
@@ -0,0 +1,192 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/gnm.py
+# Command-line driver program for Nash equilibrium computation via a global
+# Newton method
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via a global
+Newton method.
+"""
+
+from __future__ import annotations
+
+import click
+import numpy as np
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ read_strategy_profiles_csv,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria using a global Newton method"
+PROG_NAME = "gambit-gnm"
+_EXTRA_BANNER = ("Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton",)
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ default=6,
+ show_default=True,
+ type=int,
+ help="show equilibria as floating point with DECIMALS digits",
+)
+@click.option(
+ "-n",
+ "n_vectors",
+ type=int,
+ default=None,
+ help="number of perturbation vectors to generate randomly (mutually exclusive with -s)",
+)
+@click.option(
+ "-R",
+ "seed",
+ type=int,
+ default=None,
+ help="seed the random number generator used to generate perturbation vectors "
+ "(default is to seed from system entropy); requires -n",
+)
+@click.option(
+ "-s",
+ "start_file",
+ type=str,
+ default=None,
+ help="file containing perturbation vectors (mutually exclusive with -n)",
+)
+@click.option(
+ "-m",
+ "end_lambda",
+ default=-10.0,
+ show_default=True,
+ type=float,
+ help="lambda to end tracing (must be negative)",
+)
+@click.option(
+ "-f",
+ "local_newton_interval",
+ default=3,
+ show_default=True,
+ type=int,
+ help="frequency to run local Newton method",
+)
+@click.option(
+ "-i",
+ "local_newton_maxits",
+ default=10,
+ show_default=True,
+ type=int,
+ help="maximum iterations in local Newton",
+)
+@click.option(
+ "-c",
+ "steps",
+ default=100,
+ show_default=True,
+ type=int,
+ help="number of steps in each support cell",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@click.option(
+ "-V",
+ "--verbose",
+ is_flag=True,
+ help="verbose mode (shows intermediate output)",
+)
+@version_option(DESCRIPTION, _EXTRA_BANNER)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int,
+ n_vectors: int | None,
+ seed: int | None,
+ start_file: str | None,
+ end_lambda: float,
+ local_newton_interval: int,
+ local_newton_maxits: int,
+ steps: int,
+ quiet: bool,
+ verbose: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION, _EXTRA_BANNER)
+ if end_lambda >= 0.0:
+ raise ValueError("Value for -m (end lambda) must be negative")
+ if local_newton_interval <= 0:
+ raise ValueError("Value for -f (local Newton frequency) must be at least 1")
+ if local_newton_maxits <= 0:
+ raise ValueError("Value for -i (local Newton iterations) must be at least 1")
+ if steps <= 0:
+ raise ValueError("Value for -c (steps in support cell) must be at least 1")
+ if n_vectors is not None and start_file is not None:
+ raise ValueError("The -n and -s options are mutually exclusive.")
+ if seed is not None and n_vectors is None:
+ raise ValueError("The -R option requires -n.")
+ game = read_game(open_game_file(file, PROG_NAME))
+
+ if start_file is not None:
+ perturbations = read_strategy_profiles_csv(start_file, game)
+ else:
+ gen = np.random.default_rng(seed)
+ perturbations = [
+ game.random_strategy_profile(gen=gen)
+ for _ in range(n_vectors if n_vectors is not None else 1)
+ ]
+
+ def render(profile, label: str = "NE") -> None:
+ click.echo(render_profile_csv(profile, label, decimals))
+
+ def render_event(event) -> None:
+ if not verbose:
+ return
+ if isinstance(event, gbt.GNMPerturbationEvent):
+ render(event.profile, "pert")
+ elif isinstance(event, gbt.GNMStartEvent):
+ render(event.profile, "start")
+ elif isinstance(event, gbt.GNMStepEvent):
+ render(event.profile, f"{event.lam:.6g}")
+
+ for perturbation in perturbations:
+ gbt.nash.gnm_solve(
+ perturbation,
+ end_lambda=end_lambda,
+ steps=steps,
+ local_newton_interval=local_newton_interval,
+ local_newton_maxits=local_newton_maxits,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/ipa.py b/src/pygambit/cli/ipa.py
new file mode 100644
index 0000000000..9a49d5483d
--- /dev/null
+++ b/src/pygambit/cli/ipa.py
@@ -0,0 +1,140 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/ipa.py
+# Command-line driver program for Nash equilibrium computation via iterated
+# polymatrix approximation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via iterated
+polymatrix approximation.
+"""
+
+from __future__ import annotations
+
+import click
+import numpy as np
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ read_strategy_profiles_csv,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria using iterated polymatrix approximation"
+PROG_NAME = "gambit-ipa"
+_EXTRA_BANNER = ("Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton",)
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ default=6,
+ show_default=True,
+ type=int,
+ help="show equilibria as floating point with DECIMALS digits",
+)
+@click.option(
+ "-n",
+ "n_vectors",
+ type=int,
+ default=None,
+ help="number of perturbation vectors to generate randomly (mutually exclusive with -s)",
+)
+@click.option(
+ "-R",
+ "seed",
+ type=int,
+ default=None,
+ help="seed the random number generator used to generate perturbation vectors "
+ "(default is to seed from system entropy); requires -n",
+)
+@click.option(
+ "-s",
+ "start_file",
+ type=str,
+ default=None,
+ help="file containing perturbation vectors (mutually exclusive with -n)",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@click.option(
+ "-V",
+ "--verbose",
+ is_flag=True,
+ help="verbose mode (shows intermediate output)",
+)
+@version_option(DESCRIPTION, _EXTRA_BANNER)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int,
+ n_vectors: int | None,
+ seed: int | None,
+ start_file: str | None,
+ quiet: bool,
+ verbose: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION, _EXTRA_BANNER)
+ if n_vectors is not None and start_file is not None:
+ raise ValueError("The -n and -s options are mutually exclusive.")
+ if seed is not None and n_vectors is None:
+ raise ValueError("The -R option requires -n.")
+ game = read_game(open_game_file(file, PROG_NAME))
+
+ if start_file is not None:
+ perturbations = read_strategy_profiles_csv(start_file, game)
+ else:
+ gen = np.random.default_rng(seed)
+ perturbations = [
+ game.random_strategy_profile(gen=gen)
+ for _ in range(n_vectors if n_vectors is not None else 1)
+ ]
+
+ def render(profile, label: str = "NE") -> None:
+ click.echo(render_profile_csv(profile, label, decimals))
+
+ def render_event(event) -> None:
+ if not verbose:
+ return
+ if isinstance(event, gbt.IPAStepEvent):
+ render(event.profile, f"iter-{event.iteration}")
+
+ for perturbation in perturbations:
+ gbt.nash.ipa_solve(
+ perturbation,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/lcp.py b/src/pygambit/cli/lcp.py
new file mode 100644
index 0000000000..e84c551fdb
--- /dev/null
+++ b/src/pygambit/cli/lcp.py
@@ -0,0 +1,120 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/lcp.py
+# Command-line driver program for Nash equilibrium computation via LCP
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via linear
+complementarity programming.
+"""
+
+from __future__ import annotations
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ render_profile_detail,
+ validate_stop_after,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria by solving a linear complementarity program"
+PROG_NAME = "gambit-lcp"
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "For an extensive game, computes one equilibrium via the sequence form. "
+ "For a strategic game (or with -S), with no options, reports all accessible "
+ "Nash equilibria."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ type=int,
+ default=None,
+ help="compute using floating-point arithmetic; display results with DECIMALS digits",
+)
+@click.option("-S", "--strategic", is_flag=True, help="use strategic game")
+@click.option(
+ "-e",
+ "stop_after",
+ type=str,
+ default=None,
+ callback=validate_stop_after,
+ help="terminate after finding EQA equilibria "
+ "(strategic games only; default is to find all accessible equilibria)",
+)
+@click.option(
+ "-r",
+ "max_depth",
+ type=int,
+ default=0,
+ help="terminate recursion at DEPTH "
+ "(strategic games only; only if number of equilibria sought is not 1)",
+)
+@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria")
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int | None,
+ strategic: bool,
+ stop_after: int | None,
+ max_depth: int,
+ detail: bool,
+ quiet: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+ rational = decimals is None
+ render_decimals = decimals or 0
+ use_strategic = strategic or not game.is_tree
+
+ def render(profile) -> None:
+ if detail:
+ click.echo(render_profile_detail(profile, render_decimals))
+ else:
+ click.echo(render_profile_csv(profile, "NE", render_decimals))
+
+ gbt.nash.lcp_solve(
+ game,
+ rational=rational,
+ use_strategic=strategic,
+ stop_after=stop_after if use_strategic else None,
+ max_depth=max_depth if use_strategic else None,
+ nash_callback=render,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/liap.py b/src/pygambit/cli/liap.py
new file mode 100644
index 0000000000..1c75a5bb85
--- /dev/null
+++ b/src/pygambit/cli/liap.py
@@ -0,0 +1,186 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/liap.py
+# Command-line driver program for Nash equilibrium computation via
+# Lyapunov function minimization
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via Lyapunov
+function minimization.
+"""
+
+from __future__ import annotations
+
+import click
+import numpy as np
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_behavior_profiles_csv,
+ read_game,
+ read_strategy_profiles_csv,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria by minimizing the Lyapunov function"
+PROG_NAME = "gambit-liap"
+
+_DEFAULT_TRIES = 10
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, attempts to compute an equilibrium from randomly "
+ "generated starting points."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option("-A", "--agent", is_flag=True, help="compute agent form equilibria")
+@click.option(
+ "-d",
+ "decimals",
+ default=6,
+ show_default=True,
+ type=int,
+ help="print probabilities with DECIMALS digits",
+)
+@click.option(
+ "-n",
+ "n_tries",
+ type=int,
+ default=None,
+ help="number of starting points to generate randomly (mutually exclusive with -s)",
+)
+@click.option(
+ "-R",
+ "seed",
+ type=int,
+ default=None,
+ help="seed the random number generator used to generate starting points "
+ "(default is to seed from system entropy); requires -n",
+)
+@click.option(
+ "-i",
+ "maxiter",
+ default=1000,
+ show_default=True,
+ type=int,
+ help="maximum number of iterations per point",
+)
+@click.option(
+ "-m",
+ "maxregret",
+ default=1.0e-4,
+ show_default=True,
+ type=float,
+ help="maximum regret acceptable as a proportion of the range of payoffs in the game",
+)
+@click.option(
+ "-s",
+ "start_file",
+ type=str,
+ default=None,
+ help="file containing starting points (mutually exclusive with -n)",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@click.option(
+ "-V",
+ "--verbose",
+ is_flag=True,
+ help="verbose mode (shows intermediate output); default is to only show equilibria",
+)
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ agent: bool,
+ decimals: int,
+ n_tries: int | None,
+ seed: int | None,
+ maxiter: int,
+ maxregret: float,
+ start_file: str | None,
+ quiet: bool,
+ verbose: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ if n_tries is not None and start_file is not None:
+ raise ValueError("The -n and -s options are mutually exclusive.")
+ if seed is not None and n_tries is None:
+ raise ValueError("The -R option requires -n.")
+ game = read_game(open_game_file(file, PROG_NAME))
+ use_agent = agent and game.is_tree
+
+ def render(profile, label: str = "NE") -> None:
+ click.echo(render_profile_csv(profile, label, decimals))
+
+ def render_event(event) -> None:
+ if not verbose:
+ return
+ if isinstance(event, gbt.LiapStartEvent):
+ render(event.profile, "start")
+ elif isinstance(event, gbt.LiapEndEvent):
+ render(event.profile, "end")
+
+ if use_agent:
+ starts = (
+ read_behavior_profiles_csv(start_file, game)
+ if start_file is not None
+ else [
+ game.random_behavior_profile(gen=np.random.default_rng(seed))
+ for _ in range(n_tries if n_tries is not None else _DEFAULT_TRIES)
+ ]
+ )
+ for start in starts:
+ gbt.nash.liap_agent_solve(
+ start,
+ maxregret=maxregret,
+ maxiter=maxiter,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+ else:
+ starts = (
+ read_strategy_profiles_csv(start_file, game)
+ if start_file is not None
+ else [
+ game.random_strategy_profile(gen=np.random.default_rng(seed))
+ for _ in range(n_tries if n_tries is not None else _DEFAULT_TRIES)
+ ]
+ )
+ for start in starts:
+ gbt.nash.liap_solve(
+ start,
+ maxregret=maxregret,
+ maxiter=maxiter,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/logit.py b/src/pygambit/cli/logit.py
new file mode 100644
index 0000000000..9a34f2c699
--- /dev/null
+++ b/src/pygambit/cli/logit.py
@@ -0,0 +1,215 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/logit.py
+# Command-line driver program for quantal response equilibrium tracing
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for quantal response equilibrium tracing."""
+
+from __future__ import annotations
+
+import pathlib
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute a branch of the logit equilibrium correspondence"
+PROG_NAME = "gambit-logit"
+
+
+def _render_qre_row(qre, decimals: int) -> str:
+ """Render a QRE point (anything exposing `lam`, `profile`, and `log_like`, as
+ `LogitQREMixedStrategyProfile`/`LogitQREMixedBehaviorProfile` and
+ `LogitQREMixedStrategyFitResult`/`LogitQREMixedBehaviorFitResult` all do) as a
+ single comma-separated line, matching `gambit-logit`'s own `PrintProfile`: the
+ lambda label is fixed-point, the profile values use `decimals` significant
+ digits, and the log-likelihood is appended, fixed-point, when it is meaningful
+ (non-positive).
+ """
+ label = f"{qre.lam:.{decimals}f}"
+ text = render_profile_csv(qre.profile, label, decimals, fixed=False)
+ if qre.log_like <= 0.0:
+ text += f",{qre.log_like:.{decimals}f}"
+ return text
+
+
+def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDouble:
+ """Read observed strategy frequencies for maximum-likelihood estimation: a flat,
+ comma-separated list of counts, one per strategy, in the same order as a profile's
+ CSV row, matching the C++ tool's `ReadProfile`.
+ """
+ count = sum(len(list(player.strategies)) for player in game.players)
+ try:
+ fields = pathlib.Path(path).read_text().split(",")
+ values = [float(fields[i]) for i in range(count)]
+ except (OSError, ValueError, IndexError):
+ raise ValueError(f"Error reading strategy frequencies from '{path}'.") from None
+ frequencies = game.mixed_strategy_profile(rational=False)
+ it = iter(values)
+ for player in game.players:
+ frequencies[player.label] = {s.label: next(it) for s in player.strategies}
+ return frequencies
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ default=6,
+ show_default=True,
+ type=int,
+ help="show equilibria as floating point with DECIMALS digits",
+)
+@click.option("-S", "--strategic", is_flag=True, help="use strategic game")
+@click.option(
+ "-s",
+ "first_step",
+ default=0.03,
+ show_default=True,
+ type=float,
+ help="initial stepsize for the tracing procedure",
+)
+@click.option(
+ "-a",
+ "max_accel",
+ default=1.1,
+ show_default=True,
+ type=float,
+ help="maximum acceleration of the step size during the tracing procedure",
+)
+@click.option(
+ "-m",
+ "maxregret",
+ default=1.0e-8,
+ show_default=True,
+ type=float,
+ help="maximum regret acceptable as a proportion of the range of payoffs in the game",
+)
+@click.option(
+ "-l",
+ "target_lambda",
+ multiple=True,
+ type=float,
+ help="compute the QRE at LAMBDA accurately; may be specified multiple times",
+)
+@click.option(
+ "-L",
+ "mle_file",
+ type=str,
+ default=None,
+ help="compute maximum likelihood estimate; read strategy frequencies from FILE",
+)
+@click.option(
+ "-e",
+ "--terminal-only",
+ is_flag=True,
+ help="print only the terminal equilibrium (default is to print the entire branch)",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int,
+ strategic: bool,
+ first_step: float,
+ max_accel: float,
+ maxregret: float,
+ target_lambda: tuple[float, ...],
+ mle_file: str | None,
+ terminal_only: bool,
+ quiet: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+ if not game.is_perfect_recall:
+ raise ValueError("Computing equilibria of games with imperfect recall is not supported.")
+
+ def stream(qre) -> None:
+ click.echo(_render_qre_row(qre, decimals))
+
+ event_callback = None if terminal_only else stream
+
+ # Maximum-likelihood estimation, like the C++ tool, is only defined over the
+ # strategic representation, since the observed frequencies are read as a flat
+ # list of strategy counts.
+ if mle_file is not None and (strategic or not game.is_tree):
+ frequencies = _read_frequencies(mle_file, game)
+ result = gbt.qre.logit_estimate(
+ frequencies,
+ first_step=first_step,
+ max_accel=max_accel,
+ event_callback=event_callback,
+ )
+ click.echo(_render_qre_row(result, decimals))
+ return
+
+ if target_lambda:
+ for qre in gbt.qre.logit_solve_lambda(
+ game,
+ list(target_lambda),
+ use_strategic=strategic,
+ first_step=first_step,
+ max_accel=max_accel,
+ event_callback=event_callback,
+ ):
+ click.echo(_render_qre_row(qre, decimals))
+ return
+
+ if terminal_only:
+ result = gbt.nash.logit_solve(
+ game,
+ use_strategic=strategic,
+ maxregret=maxregret,
+ first_step=first_step,
+ max_accel=max_accel,
+ )
+ click.echo(render_profile_csv(result.equilibria[-1], "NE", decimals, fixed=False))
+ return
+
+ result = gbt.nash.logit_solve(
+ game,
+ use_strategic=strategic,
+ maxregret=maxregret,
+ first_step=first_step,
+ max_accel=max_accel,
+ event_callback=stream,
+ )
+ click.echo(render_profile_csv(result.equilibria[-1], "NE", decimals, fixed=False))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/lp.py b/src/pygambit/cli/lp.py
new file mode 100644
index 0000000000..3e388423eb
--- /dev/null
+++ b/src/pygambit/cli/lp.py
@@ -0,0 +1,95 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/lp.py
+# Command-line driver program for Nash equilibrium computation via LP
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via linear
+programming, for two-player constant-sum games.
+"""
+
+from __future__ import annotations
+
+import click
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ render_profile_csv,
+ render_profile_detail,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria by solving a linear program"
+PROG_NAME = "gambit-lp"
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, computes one Nash equilibrium."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-d",
+ "decimals",
+ type=int,
+ default=None,
+ help="compute using floating-point arithmetic; display results with DECIMALS digits",
+)
+@click.option("-S", "--strategic", is_flag=True, help="use strategic game")
+@click.option("-D", "--detail", is_flag=True, help="print detailed information about equilibria")
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ decimals: int | None,
+ strategic: bool,
+ detail: bool,
+ quiet: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ game = read_game(open_game_file(file, PROG_NAME))
+ rational = decimals is None
+ render_decimals = decimals or 0
+
+ def render(profile) -> None:
+ if detail:
+ click.echo(render_profile_detail(profile, render_decimals))
+ else:
+ click.echo(render_profile_csv(profile, "NE", render_decimals))
+
+ gbt.nash.lp_solve(
+ game,
+ rational=rational,
+ use_strategic=strategic,
+ nash_callback=render,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/cli/simpdiv.py b/src/pygambit/cli/simpdiv.py
new file mode 100644
index 0000000000..1f929e94e7
--- /dev/null
+++ b/src/pygambit/cli/simpdiv.py
@@ -0,0 +1,185 @@
+#
+# This file is part of Gambit
+# Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
+#
+# FILE: src/pygambit/cli/simpdiv.py
+# Command-line driver program for Nash equilibrium computation via
+# simplicial subdivision
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+"""Command-line driver program for Nash equilibrium computation via simplicial
+subdivision.
+"""
+
+from __future__ import annotations
+
+import click
+import numpy as np
+
+import pygambit as gbt
+
+from .common import (
+ handle_errors,
+ open_game_file,
+ print_banner,
+ read_game,
+ read_strategy_profiles_csv,
+ render_profile_csv,
+ version_option,
+)
+
+DESCRIPTION = "Compute Nash equilibria using simplicial subdivision"
+PROG_NAME = "gambit-simpdiv"
+
+
+def _default_start(game: gbt.Game) -> gbt.MixedStrategyProfileRational:
+ """Each player's first strategy, matching the C++ library's `SimpdivDefaultStart`."""
+ start = game.mixed_strategy_profile(rational=True)
+ for player in game.players:
+ first_strategy = next(iter(player.strategies))
+ start[player.label] = {first_strategy.label: 1}
+ return start
+
+
+@click.command(
+ context_settings={"help_option_names": ["-h", "--help"]},
+ help=(
+ f"{DESCRIPTION}.\n\n"
+ "Reads a game from FILE, or from standard input if FILE is not specified. "
+ "With no options, computes one approximate Nash equilibrium."
+ ),
+)
+@click.argument("file", required=False, default=None)
+@click.option(
+ "-g",
+ "refine",
+ default=2,
+ show_default=True,
+ type=int,
+ help="granularity of grid refinement at each step",
+)
+@click.option(
+ "-r",
+ "rand_denom",
+ type=int,
+ default=None,
+ help="generate random starting points with denominator DENOM (mutually exclusive with -s)",
+)
+@click.option(
+ "-n",
+ "n_tries",
+ type=int,
+ default=None,
+ help="number of starting points to generate (requires -r)",
+)
+@click.option(
+ "-R",
+ "seed",
+ type=int,
+ default=None,
+ help="seed the random number generator used to generate starting points "
+ "(default is to seed from system entropy); requires -n",
+)
+@click.option(
+ "-s",
+ "start_file",
+ type=str,
+ default=None,
+ help="file containing starting points (mutually exclusive with -r)",
+)
+@click.option(
+ "-d",
+ "decimals",
+ default=0,
+ show_default=True,
+ type=int,
+ help="show profiles as floating point with DECIMALS digits "
+ "(default is to display rational numbers)",
+)
+@click.option(
+ "-m",
+ "maxregret",
+ type=str,
+ default=None,
+ help="maximum regret acceptable as a proportion of range of payoffs in the game "
+ "(default is 1/10000000)",
+)
+@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
+@click.option(
+ "-V",
+ "--verbose",
+ is_flag=True,
+ help="verbose mode (shows intermediate output); default is to only show equilibria",
+)
+@version_option(DESCRIPTION)
+@handle_errors
+def main(
+ file: str | None,
+ refine: int,
+ rand_denom: int | None,
+ n_tries: int | None,
+ seed: int | None,
+ start_file: str | None,
+ decimals: int,
+ maxregret: str | None,
+ quiet: bool,
+ verbose: bool,
+) -> None:
+ if not quiet:
+ print_banner(DESCRIPTION)
+ if rand_denom is not None and start_file is not None:
+ raise ValueError("The -r and -s options are mutually exclusive.")
+ if n_tries is not None and rand_denom is None:
+ raise ValueError("The -n option requires -r.")
+ if seed is not None and n_tries is None:
+ raise ValueError("The -R option requires -n.")
+ game = read_game(open_game_file(file, PROG_NAME))
+ maxregret_value = gbt.Rational(maxregret) if maxregret is not None else None
+ as_float = decimals > 0
+
+ if start_file is not None:
+ starts = read_strategy_profiles_csv(start_file, game)
+ elif rand_denom is not None:
+ starts = [
+ game.random_strategy_profile(denom=rand_denom, gen=np.random.default_rng(seed))
+ for _ in range(n_tries if n_tries is not None else 1)
+ ]
+ else:
+ starts = [_default_start(game)]
+
+ def render(profile, label: str = "NE") -> None:
+ click.echo(render_profile_csv(profile, label, decimals, as_float=as_float))
+
+ def render_event(event) -> None:
+ if not verbose:
+ return
+ if isinstance(event, gbt.SimpdivStartEvent):
+ render(event.profile, "start")
+ elif isinstance(event, gbt.SimpdivRefinementEvent):
+ render(event.profile, f"{float(event.grid_size):.6f}")
+
+ for start in starts:
+ gbt.nash.simpdiv_solve(
+ start,
+ maxregret=maxregret_value,
+ refine=refine,
+ nash_callback=render,
+ event_callback=render_event,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/pygambit/gambit.pxd b/src/pygambit/gambit.pxd
index fe51bae0e8..2071cd60a5 100644
--- a/src/pygambit/gambit.pxd
+++ b/src/pygambit/gambit.pxd
@@ -6,6 +6,7 @@ from libcpp.vector cimport vector as stdvector
from libcpp.set cimport set as stdset
from libcpp.map cimport map as stdmap
from libcpp.optional cimport optional
+from libcpp.pair cimport pair
cdef extern from "games.h":
@@ -690,6 +691,11 @@ cdef extern from "solvers/logit/logit.h":
cdef extern from "nash.h":
+ pair[
+ stdlist[c_MixedStrategyProfile[T]], stdlist[stdlist[c_MixedStrategyProfile[T]]]
+ ] EnumMixedStrategySolveCliquesWrapper[T](
+ c_Game, StrategyCallbackType[T]
+ ) except +RuntimeError
stdlist[c_MixedBehaviorProfile[double]] LogitBehaviorSolveWrapper(
c_Game, double, double, double, LogitEventCallbackType[c_LogitQREMixedBehaviorProfile]
) except +
@@ -697,10 +703,12 @@ cdef extern from "nash.h":
c_Game, double, double, double
) except +
stdlist[shared_ptr[c_LogitQREMixedBehaviorProfile]] LogitBehaviorAtLambdaWrapper(
- c_Game, stdlist[double], double, double
+ c_Game, stdlist[double], double, double,
+ LogitEventCallbackType[c_LogitQREMixedBehaviorProfile]
) except +
shared_ptr[c_LogitQREMixedBehaviorProfile] LogitBehaviorEstimateWrapper(
- shared_ptr[c_MixedBehaviorProfile[double]], bool, double, double
+ shared_ptr[c_MixedBehaviorProfile[double]], bool, double, double,
+ LogitEventCallbackType[c_LogitQREMixedBehaviorProfile]
) except +
stdlist[c_MixedStrategyProfile[double]] LogitStrategySolveWrapper(
c_Game, double, double, double, LogitEventCallbackType[c_LogitQREMixedStrategyProfile]
@@ -709,8 +717,10 @@ cdef extern from "nash.h":
c_Game, double, double, double
) except +
stdlist[shared_ptr[c_LogitQREMixedStrategyProfile]] LogitStrategyAtLambdaWrapper(
- c_Game, stdlist[double], double, double
+ c_Game, stdlist[double], double, double,
+ LogitEventCallbackType[c_LogitQREMixedStrategyProfile]
) except +
shared_ptr[c_LogitQREMixedStrategyProfile] LogitStrategyEstimateWrapper(
- shared_ptr[c_MixedStrategyProfile[double]], bool, double, double
+ shared_ptr[c_MixedStrategyProfile[double]], bool, double, double,
+ LogitEventCallbackType[c_LogitQREMixedStrategyProfile]
) except +
diff --git a/src/pygambit/nash.h b/src/pygambit/nash.h
index 1e3b3609a5..96091b85b8 100644
--- a/src/pygambit/nash.h
+++ b/src/pygambit/nash.h
@@ -20,11 +20,26 @@
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//
+#include "solvers/enummixed/enummixed.h"
#include "solvers/logit/logit.h"
using namespace std;
using namespace Gambit;
+template
+std::pair>, std::list>>>
+EnumMixedStrategySolveCliquesWrapper(
+ const Game &p_game,
+ Nash::StrategyCallbackType p_onEquilibrium = Nash::NullStrategyCallback)
+{
+ auto solution = Nash::EnumMixedStrategySolveDetailed(p_game, p_onEquilibrium);
+ std::list>> cliques;
+ for (auto &clique : solution->GetCliques()) {
+ cliques.emplace_back(clique.begin(), clique.end());
+ }
+ return {solution->GetExtremeEquilibria(), cliques};
+}
+
std::list>
LogitBehaviorSolveWrapper(const Game &p_game, double p_regret, double p_firstStep,
double p_maxAccel,
@@ -50,19 +65,24 @@ LogitBehaviorPrincipalBranchWrapper(const Game &p_game, double p_regret, double
std::shared_ptr
LogitBehaviorEstimateWrapper(std::shared_ptr> p_frequencies,
- bool p_stopAtLocal, double p_firstStep, double p_maxAccel)
+ bool p_stopAtLocal, double p_firstStep, double p_maxAccel,
+ LogitEventCallbackType p_onEvent =
+ NullLogitEventCallback)
{
return make_shared(LogitBehaviorEstimate(
- *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel));
+ *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel, p_onEvent));
}
std::list>
LogitBehaviorAtLambdaWrapper(const Game &p_game, const std::list &p_targetLambda,
- double p_firstStep, double p_maxAccel)
+ double p_firstStep, double p_maxAccel,
+ LogitEventCallbackType p_onEvent =
+ NullLogitEventCallback)
{
LogitQREMixedBehaviorProfile start(p_game);
std::list> ret;
- for (auto &qre : LogitBehaviorSolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel)) {
+ for (auto &qre :
+ LogitBehaviorSolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel, p_onEvent)) {
ret.push_back(std::make_shared(qre));
}
return ret;
@@ -93,11 +113,14 @@ LogitStrategyPrincipalBranchWrapper(const Game &p_game, double p_regret, double
std::list>
LogitStrategyAtLambdaWrapper(const Game &p_game, const std::list &p_targetLambda,
- double p_firstStep, double p_maxAccel)
+ double p_firstStep, double p_maxAccel,
+ LogitEventCallbackType p_onEvent =
+ NullLogitEventCallback)
{
LogitQREMixedStrategyProfile start(p_game);
std::list> ret;
- for (auto &qre : LogitStrategySolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel)) {
+ for (auto &qre :
+ LogitStrategySolveLambda(start, p_targetLambda, 1.0, p_firstStep, p_maxAccel, p_onEvent)) {
ret.push_back(std::make_shared(qre));
}
return ret;
@@ -105,8 +128,10 @@ LogitStrategyAtLambdaWrapper(const Game &p_game, const std::list &p_targ
std::shared_ptr
LogitStrategyEstimateWrapper(std::shared_ptr> p_frequencies,
- bool p_stopAtLocal, double p_firstStep, double p_maxAccel)
+ bool p_stopAtLocal, double p_firstStep, double p_maxAccel,
+ LogitEventCallbackType p_onEvent =
+ NullLogitEventCallback)
{
return make_shared(LogitStrategyEstimate(
- *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel));
+ *p_frequencies, 1000000.0, 1.0, p_stopAtLocal, p_firstStep, p_maxAccel, p_onEvent));
}
diff --git a/src/pygambit/nash.pxi b/src/pygambit/nash.pxi
index 2e202146df..d3b471c7fe 100644
--- a/src/pygambit/nash.pxi
+++ b/src/pygambit/nash.pxi
@@ -471,6 +471,35 @@ def _enummixed_strategy_solve_rational(
)
+def _enummixed_strategy_solve_cliques_double(
+ game: Game, nash_callback: object = None
+) -> tuple[list[MixedStrategyProfileDouble], list[list[MixedStrategyProfileDouble]]]:
+ result: pair[
+ stdlist[c_MixedStrategyProfile[float]], stdlist[stdlist[c_MixedStrategyProfile[float]]]
+ ] = EnumMixedStrategySolveCliquesWrapper[double](
+ game.game, MakeStrategyCallback[double](nash_callback)
+ )
+ return (
+ _convert_mspd(result.first),
+ [_convert_mspd(clique) for clique in result.second],
+ )
+
+
+def _enummixed_strategy_solve_cliques_rational(
+ game: Game, nash_callback: object = None
+) -> tuple[list[MixedStrategyProfileRational], list[list[MixedStrategyProfileRational]]]:
+ result: pair[
+ stdlist[c_MixedStrategyProfile[c_Rational]],
+ stdlist[stdlist[c_MixedStrategyProfile[c_Rational]]]
+ ] = EnumMixedStrategySolveCliquesWrapper[c_Rational](
+ game.game, MakeStrategyCallback[c_Rational](nash_callback)
+ )
+ return (
+ _convert_mspr(result.first),
+ [_convert_mspr(clique) for clique in result.second],
+ )
+
+
def _lcp_behavior_solve_double(
game: Game, nash_callback: object = None
) -> list[MixedBehaviorProfileDouble]:
@@ -737,19 +766,24 @@ class LogitQREMixedStrategyProfile:
def _logit_strategy_estimate(profile: MixedStrategyProfileDouble,
local_max: bool = False,
first_step: float = .03,
- max_accel: float = 1.1) -> LogitQREMixedStrategyProfile:
+ max_accel: float = 1.1,
+ event_callback: object = None) -> LogitQREMixedStrategyProfile:
"""Estimate QRE corresponding to mixed strategy profile using
maximum likelihood along the principal branch.
"""
return LogitQREMixedStrategyProfile.wrap(
- LogitStrategyEstimateWrapper(profile.profile, local_max, first_step, max_accel)
+ LogitStrategyEstimateWrapper(
+ profile.profile, local_max, first_step, max_accel,
+ MakeLogitEventCallback[c_LogitQREMixedStrategyProfile](event_callback)
+ )
)
def _logit_strategy_lambda(game: Game,
lam: float | list[float],
first_step: float = .03,
- max_accel: float = 1.1) -> list[LogitQREMixedStrategyProfile]:
+ max_accel: float = 1.1,
+ event_callback: object = None) -> list[LogitQREMixedStrategyProfile]:
"""Compute the first QRE encountered along the principal branch of the strategic
game corresponding to lambda value `lam`.
"""
@@ -758,7 +792,10 @@ def _logit_strategy_lambda(game: Game,
except TypeError:
lam = [lam]
return [LogitQREMixedStrategyProfile.wrap(profile)
- for profile in LogitStrategyAtLambdaWrapper(game.game, lam, first_step, max_accel)]
+ for profile in LogitStrategyAtLambdaWrapper(
+ game.game, lam, first_step, max_accel,
+ MakeLogitEventCallback[c_LogitQREMixedStrategyProfile](event_callback)
+ )]
def _logit_strategy_branch(game: Game,
@@ -814,19 +851,24 @@ class LogitQREMixedBehaviorProfile:
def _logit_behavior_estimate(profile: MixedBehaviorProfileDouble,
local_max: bool = False,
first_step: float = .03,
- max_accel: float = 1.1) -> LogitQREMixedBehaviorProfile:
+ max_accel: float = 1.1,
+ event_callback: object = None) -> LogitQREMixedBehaviorProfile:
"""Estimate QRE corresponding to mixed behavior profile using
maximum likelihood along the principal branch.
"""
ret = LogitQREMixedBehaviorProfile(profile.game)
- ret.thisptr = LogitBehaviorEstimateWrapper(profile.profile, local_max, first_step, max_accel)
+ ret.thisptr = LogitBehaviorEstimateWrapper(
+ profile.profile, local_max, first_step, max_accel,
+ MakeLogitEventCallback[c_LogitQREMixedBehaviorProfile](event_callback)
+ )
return ret
def _logit_behavior_lambda(game: Game,
lam: float | list[float],
first_step: float = .03,
- max_accel: float = 1.1) -> list[LogitQREMixedBehaviorProfile]:
+ max_accel: float = 1.1,
+ event_callback: object = None) -> list[LogitQREMixedBehaviorProfile]:
"""Compute the first QRE encountered along the principal branch of the extensive
game corresponding to lambda value `lam`.
"""
@@ -835,7 +877,10 @@ def _logit_behavior_lambda(game: Game,
except TypeError:
lam = [lam]
ret = []
- for profile in LogitBehaviorAtLambdaWrapper(game.game, lam, first_step, max_accel):
+ for profile in LogitBehaviorAtLambdaWrapper(
+ game.game, lam, first_step, max_accel,
+ MakeLogitEventCallback[c_LogitQREMixedBehaviorProfile](event_callback)
+ ):
qre = LogitQREMixedBehaviorProfile()
qre.thisptr = profile
ret.append(qre)
diff --git a/src/pygambit/nash.py b/src/pygambit/nash.py
index 15cdc86f20..d3f8beef9c 100644
--- a/src/pygambit/nash.py
+++ b/src/pygambit/nash.py
@@ -158,6 +158,7 @@ def enummixed_solve(
nash_callback: Callable[
[libgbt.MixedStrategyProfile], None
] | None = None,
+ cliques: bool = False,
) -> NashComputationResult:
"""Compute all :ref:`mixed-strategy Nash equilibria `
of a two-player game using the strategic representation.
@@ -184,6 +185,13 @@ def enummixed_solve(
.. versionadded:: 17.0.0
+ cliques : bool, default False
+ If specified and True, also compute the sets of extreme equilibria which
+ are connected to one another, returned as a list of lists of equilibria in
+ `parameters["cliques"]`. Not available when `lrsnash_path` is specified.
+
+ .. versionadded:: 17.0.0
+
Returns
-------
res : NashComputationResult
@@ -195,7 +203,8 @@ def enummixed_solve(
If game has more than two players.
ValueError
- If both `lrsnash_path` and `nash_callback` are specified.
+ If both `lrsnash_path` and `nash_callback` are specified, or both
+ `lrsnash_path` and `cliques` are specified.
Notes
-----
@@ -206,6 +215,10 @@ def enummixed_solve(
raise ValueError(
"enummixed_solve(): nash_callback cannot be used with lrsnash_path"
)
+ if cliques:
+ raise ValueError(
+ "enummixed_solve(): cliques cannot be used with lrsnash_path"
+ )
equilibria = nashlrs.lrsnash_solve(game, lrsnash_path=lrsnash_path)
return NashComputationResult(
game=game,
@@ -215,6 +228,23 @@ def enummixed_solve(
parameters={"lrsnash_path": lrsnash_path},
equilibria=equilibria,
)
+ if cliques:
+ if rational:
+ equilibria, clique_list = libgbt._enummixed_strategy_solve_cliques_rational(
+ game, nash_callback
+ )
+ else:
+ equilibria, clique_list = libgbt._enummixed_strategy_solve_cliques_double(
+ game, nash_callback
+ )
+ return NashComputationResult(
+ game=game,
+ method="enummixed",
+ rational=rational,
+ use_strategic=True,
+ equilibria=equilibria,
+ parameters={"cliques": clique_list},
+ )
if rational:
equilibria = libgbt._enummixed_strategy_solve_rational(game, nash_callback)
else:
diff --git a/src/pygambit/node.pxi b/src/pygambit/node.pxi
index fc6e1b19af..42f441f357 100644
--- a/src/pygambit/node.pxi
+++ b/src/pygambit/node.pxi
@@ -385,6 +385,13 @@ class Node:
def label(self, value: str) -> None:
self.node.deref().SetLabel(value.encode("utf-8"))
+ @property
+ def number(self) -> int:
+ """Returns the number of the node in its game.
+ Nodes are numbered starting with 0.
+ """
+ return self.node.deref().GetNumber() - 1
+
@property
def children(self) -> NodeChildren:
"""The set of children of this node."""
diff --git a/src/pygambit/qre.py b/src/pygambit/qre.py
index 0e2dd127c0..e5cb93b0bf 100644
--- a/src/pygambit/qre.py
+++ b/src/pygambit/qre.py
@@ -25,6 +25,7 @@
from __future__ import annotations
import math
+from collections.abc import Callable
import scipy.optimize
@@ -56,15 +57,32 @@ def logit_solve_lambda(
use_strategic: bool = False,
first_step: float = .03,
max_accel: float = 1.1,
+ event_callback: Callable[
+ [libgbt.LogitQREMixedStrategyProfile | libgbt.LogitQREMixedBehaviorProfile], None
+ ] | None = None,
):
+ """Compute the QRE(s) at the specified value(s) of `lam` along the principal branch.
+
+ Parameters
+ ----------
+ event_callback : Callable, optional
+ If specified, called with each point traced along the principal branch on the
+ way to each requested value of `lam`.
+
+ .. versionadded:: 17.0.0
+ """
if first_step <= 0.0:
raise ValueError("logit_solve_lambda(): first_step argument must be positive")
if max_accel < 1.0:
raise ValueError("logit_solve_lambda(): max_accel argument must be at least 1.0")
if not game.is_tree or use_strategic:
- return libgbt._logit_strategy_lambda(game, lam, first_step, max_accel)
+ return libgbt._logit_strategy_lambda(
+ game, lam, first_step, max_accel, event_callback
+ )
else:
- return libgbt._logit_behavior_lambda(game, lam, first_step, max_accel)
+ return libgbt._logit_behavior_lambda(
+ game, lam, first_step, max_accel, event_callback
+ )
class LogitQREMixedStrategyFitResult:
@@ -166,9 +184,11 @@ def _estimate_strategy_fixedpoint(
local_max: bool = False,
first_step: float = .03,
max_accel: float = 1.1,
+ event_callback: object = None,
) -> LogitQREMixedStrategyFitResult:
res = libgbt._logit_strategy_estimate(data.as_float(), local_max=local_max,
- first_step=first_step, max_accel=max_accel)
+ first_step=first_step, max_accel=max_accel,
+ event_callback=event_callback)
return LogitQREMixedStrategyFitResult(
data, "fixedpoint", res.lam, res.profile, res.log_like
)
@@ -179,9 +199,11 @@ def _estimate_behavior_fixedpoint(
local_max: bool = False,
first_step: float = .03,
max_accel: float = 1.1,
+ event_callback: object = None,
) -> LogitQREMixedBehaviorFitResult:
res = libgbt._logit_behavior_estimate(data.as_float(), local_max=local_max,
- first_step=first_step, max_accel=max_accel)
+ first_step=first_step, max_accel=max_accel,
+ event_callback=event_callback)
return LogitQREMixedBehaviorFitResult(
data, "fixedpoint", res.lam, res.profile, res.log_like
)
@@ -251,9 +273,7 @@ def _estimate_behavior_empirical(
for player in data.game.players:
for infoset in player.infosets:
node = next(iter(infoset.members))
- profile[node] = {
- a.label: math.exp(next(log_probs)) for a in infoset.actions
- }
+ profile[node] = {a.label: math.exp(next(log_probs)) for a in infoset.actions}
return LogitQREMixedBehaviorFitResult(
data, "empirical", res.x[0], profile, -res.fun
)
@@ -265,6 +285,9 @@ def logit_estimate(
local_max: bool = False,
first_step: float = .03,
max_accel: float = 1.1,
+ event_callback: Callable[
+ [libgbt.LogitQREMixedStrategyProfile | libgbt.LogitQREMixedBehaviorProfile], None
+ ] | None = None,
) -> LogitQREMixedStrategyFitResult | LogitQREMixedBehaviorFitResult:
"""Use maximum likelihood estimation to find the logit quantal
response equilibrium which best fits empirical frequencies of play.
@@ -322,6 +345,17 @@ def logit_estimate(
This argument only has an effect when use_empirical is False.
+ event_callback : Callable[[LogitQREMixedStrategyProfile | LogitQREMixedBehaviorProfile], \
+None], optional
+ If specified, called with each point traced along the principal branch on the
+ way to the best-fitting QRE.
+
+ .. note::
+
+ This argument only has an effect when use_empirical is False.
+
+ .. versionadded:: 17.0.0
+
Returns
-------
LogitQREMixedStrategyFitResult or LogitQREMixedBehaviorFitResult
@@ -335,17 +369,23 @@ def logit_estimate(
as a structural model for estimation: The missing manual.
SSRN working paper 4425515.
"""
+ if use_empirical and event_callback is not None:
+ raise ValueError(
+ "logit_estimate(): event_callback cannot be used with use_empirical"
+ )
if isinstance(data, libgbt.MixedStrategyProfile):
if use_empirical:
return _estimate_strategy_empirical(data)
else:
return _estimate_strategy_fixedpoint(data, local_max=local_max,
- first_step=first_step, max_accel=max_accel)
+ first_step=first_step, max_accel=max_accel,
+ event_callback=event_callback)
elif isinstance(data, libgbt.MixedBehaviorProfile):
if use_empirical:
return _estimate_behavior_empirical(data)
else:
return _estimate_behavior_fixedpoint(data, local_max=local_max,
- first_step=first_step, max_accel=max_accel)
+ first_step=first_step, max_accel=max_accel,
+ event_callback=event_callback)
else:
raise TypeError("data must be specified as a MixedStrategyProfile or MixedBehaviorProfile")
diff --git a/src/tools/convert/convert.cc b/src/tools/convert/convert.cc
deleted file mode 100644
index 4b6c44fb4c..0000000000
--- a/src/tools/convert/convert.cc
+++ /dev/null
@@ -1,150 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/convert/convert.cc
-// Convert among various file formats
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-
-#include "games.h"
-#include "games/writer.h"
-
-using namespace Gambit;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Convert games among various file formats\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS]\n";
- std::cerr << "Accepts input file on standard input.\n";
- std::cerr << "Converts games among various file formats\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -O FORMAT output file format (required):\n";
- std::cerr << " FORMAT=html convert to HTML\n";
- std::cerr << " FORMAT=sgame convert to LaTeX sgame style\n";
- std::cerr << " -c PLAYER the player to show on columns (default is 2)\n";
- std::cerr << " -r PLAYER the player to show on rows (default is 1)\n";
- std::cerr << " -h print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- int c;
- int rowPlayer = 1, colPlayer = 2;
- bool quiet = false;
- std::string format;
-
- while ((c = getopt(argc, argv, "r:c:hqO:")) != -1) {
- switch (c) {
- case 'r':
- rowPlayer = atoi(optarg);
- break;
- case 'c':
- colPlayer = atoi(optarg);
- break;
- case 'O':
- format = optarg;
- break;
- case 'q':
- quiet = true;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- if (format.empty()) {
- std::cerr << argv[0] << ": Output format argument -O required.\n";
- return 1;
- }
- else if (format != "sgame" && format != "html") {
- std::cerr << argv[0] << ": Unknown output format '" << format << "'.\n";
- return 1;
- }
-
- if (rowPlayer == colPlayer) {
- std::cerr << argv[0] << ": Row and column players must be different.\n";
- return 1;
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
-
- if (rowPlayer < 1 || rowPlayer > static_cast(game->NumPlayers())) {
- std::cerr << argv[0] << ": Player " << rowPlayer << " does not exist.\n";
- return 1;
- }
- if (colPlayer < 1 || colPlayer > static_cast(game->NumPlayers())) {
- std::cerr << argv[0] << ": Player " << colPlayer << " does not exist.\n";
- return 1;
- }
-
- if (format == "html") {
- std::cout << WriteHTMLFile(game, game->GetPlayer(rowPlayer), game->GetPlayer(colPlayer));
- }
- else {
- std::cout << WriteLaTeXFile(game, game->GetPlayer(rowPlayer), game->GetPlayer(colPlayer));
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/enummixed/enummixed.cc b/src/tools/enummixed/enummixed.cc
deleted file mode 100644
index d16369a785..0000000000
--- a/src/tools/enummixed/enummixed.cc
+++ /dev/null
@@ -1,153 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/enummixed/enummixed.cc
-// Compute Nash equilibria via Mangasarian's algorithm
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/enummixed/enummixed.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-template
-void PrintCliques(const Array>> &p_cliques,
- std::shared_ptr> p_renderer)
-{
- for (auto [cl, clique] : enumerate(p_cliques)) {
- const std::string label = "convex-" + std::to_string(cl + 1);
- for (const auto &profile : clique) {
- p_renderer->Render(profile, label);
- }
- }
-}
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria by enumerating extreme points\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, reports all Nash equilibria found.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS compute using floating-point arithmetic;\n";
- std::cerr << " display results with DECIMALS digits\n";
- std::cerr << " -c output connectedness information\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- int c;
- bool useFloat = false, quiet = false;
- bool showConnect = false;
- int numDecimals = 6;
-
- int long_opt_index = 0;
- option long_options[] = {
- {"help", 0, nullptr, 'h'}, {"version", 0, nullptr, 'v'}, {nullptr, 0, nullptr, 0}};
- while ((c = getopt_long(argc, argv, "d:vhqc", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'd':
- useFloat = true;
- numDecimals = atoi(optarg);
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'c':
- showConnect = true;
- break;
- case 'q':
- quiet = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (useFloat) {
- auto renderer = MakeMixedStrategyProfileRenderer(std::cout, numDecimals, false);
- auto solution = EnumMixedStrategySolveDetailed(
- game, [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- if (showConnect) {
- PrintCliques(solution->GetCliques(), renderer);
- }
- }
- else {
- auto renderer = MakeMixedStrategyProfileRenderer(std::cout, numDecimals, false);
- auto solution = EnumMixedStrategySolveDetailed(
- game, [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- if (showConnect) {
- PrintCliques(solution->GetCliques(), renderer);
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/enumpoly/enumpoly.cc b/src/tools/enumpoly/enumpoly.cc
deleted file mode 100644
index 0539629ff7..0000000000
--- a/src/tools/enumpoly/enumpoly.cc
+++ /dev/null
@@ -1,256 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/enumpoly/enumpoly.cc
-// Enumerates all Nash equilibria via support enumeration
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "solvers/enumpoly/enumpoly.h"
-
-using namespace Gambit;
-using namespace Nash;
-
-int g_numDecimals = 6;
-bool g_verbose = false;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria by solving polynomial systems\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, reports all Nash equilibria found.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS show equilibrium probabilities with DECIMALS digits\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -S use strategic game\n";
- std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
- std::cerr << " payoffs in the game (default is 1e-8)\n";
- std::cerr << " -e EQA terminate after finding EQA equilibria\n";
- std::cerr << " (default is to search in all supports)\n";
- std::cerr << " -r RECTANGLES maximum number of rectangles to examine when searching\n";
- std::cerr << " for roots on a single support, before giving up on that\n";
- std::cerr << " support (default " << Nash::kDefaultEnumPolyMaxRectangles
- << ")\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -V, --verbose verbose mode (shows supports investigated)\n";
- std::cerr << " (default is only to show equilibria)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-void PrintProfile(std::ostream &p_stream, const std::string &p_label,
- const MixedStrategyProfile &p_profile)
-{
- p_stream << p_label;
- for (size_t i = 1; i <= p_profile.MixedProfileLength(); i++) {
- p_stream.setf(std::ios::fixed);
- p_stream << ',' << std::setprecision(g_numDecimals) << p_profile[i];
- }
- p_stream << std::endl;
-}
-
-void PrintSupport(std::ostream &p_stream, const std::string &p_label,
- const StrategySupportProfile &p_support)
-{
- if (!g_verbose) {
- return;
- }
- p_stream << p_label;
- for (auto player : p_support.GetGame()->GetPlayers()) {
- p_stream << ",";
- for (auto strategy : player->GetStrategies()) {
- p_stream << ((p_support.Contains(strategy)) ? 1 : 0);
- }
- }
- p_stream << std::endl;
-}
-
-void PrintProfile(std::ostream &p_stream, const std::string &p_label,
- const MixedBehaviorProfile &p_profile)
-{
- p_stream << p_label;
- for (size_t i = 1; i <= p_profile.BehaviorProfileLength(); i++) {
- p_stream.setf(std::ios::fixed);
- p_stream << "," << std::setprecision(g_numDecimals) << p_profile[i];
- }
- p_stream << std::endl;
-}
-
-void PrintSupport(std::ostream &p_stream, const std::string &p_label,
- const BehaviorSupportProfile &p_support)
-{
- if (!g_verbose) {
- return;
- }
- p_stream << p_label;
- for (auto player : p_support.GetGame()->GetPlayers()) {
- for (auto infoset : player->GetInfosets()) {
- p_stream << ",";
- for (auto action : infoset->GetActions()) {
- p_stream << ((p_support.Contains(action)) ? 1 : 0);
- }
- }
- }
- p_stream << std::endl;
-}
-
-template void PrintEnumPolyEvent(const EnumPolyEvent &p_event)
-{
- std::visit(
- [](const Event &event) {
- if constexpr (std::is_same_v>) {
- PrintSupport(std::cout, "candidate", event.support);
- }
- else if constexpr (std::is_same_v>) {
- PrintSupport(std::cout, "singular", event.support);
- }
- else if constexpr (std::is_same_v>) {
- PrintSupport(std::cout, "budget-exceeded", event.support);
- }
- },
- p_event);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
-
- bool quiet = false;
- bool useStrategic = false;
- double maxregret = 1.0e-8;
- std::optional stopAfter;
- size_t maxRectangles = Nash::kDefaultEnumPolyMaxRectangles;
-
- int long_opt_index = 0;
- option long_options[] = {{"help", 0, nullptr, 'h'},
- {"version", 0, nullptr, 'v'},
- {"verbose", 0, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "d:hSm:e:r:qvV", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'd':
- g_numDecimals = atoi(optarg);
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'S':
- useStrategic = true;
- break;
- case 'm':
- maxregret = atof(optarg);
- break;
- case 'e': {
- size_t parsed;
- const auto *begin = optarg;
- const auto *end = optarg + std::strlen(optarg);
- const auto result = std::from_chars(begin, end, parsed);
- if (result.ec != std::errc{} || result.ptr != end || parsed == 0) {
- std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'."
- << std::endl;
- exit(1);
- }
- stopAfter = parsed;
- break;
- }
- case 'r':
- maxRectangles = std::strtoull(optarg, nullptr, 10);
- break;
- case 'q':
- quiet = true;
- break;
- case 'V':
- g_verbose = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (!game->IsPerfectRecall()) {
- throw UndefinedException(
- "Computing equilibria of games with imperfect recall is not supported.");
- }
-
- if (!game->IsTree() || useStrategic) {
- EnumPolyStrategySolve(
- game, stopAfter, maxregret, maxRectangles,
- [](const MixedStrategyProfile &eqm) { PrintProfile(std::cout, "NE", eqm); },
- [](const EnumPolyEvent &event) { PrintEnumPolyEvent(event); });
- }
- else {
- EnumPolyBehaviorSolve(
- game, stopAfter, maxregret, maxRectangles,
- [](const MixedBehaviorProfile &eqm) { PrintProfile(std::cout, "NE", eqm); },
- [](const EnumPolyEvent &event) { PrintEnumPolyEvent(event); });
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/enumpure/enumpure.cc b/src/tools/enumpure/enumpure.cc
deleted file mode 100644
index 72c3ed84f3..0000000000
--- a/src/tools/enumpure/enumpure.cc
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/enumpure/enumpure.cc
-// Compute pure-strategy equilibria in extensive form games
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/enumpure/enumpure.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Search for Nash equilibria in pure strategies\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, locates all Nash equilibria in pure strategies.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -S report equilibria in strategies even for extensive games\n";
- std::cerr << " -A compute agent form equilibria\n";
- std::cerr << " -D print detailed information about equilibria\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- bool quiet = false, reportStrategic = false, solveAgent = false;
- bool printDetail = false;
-
- int long_opt_index = 0;
- option long_options[] = {
- {"help", 0, nullptr, 'h'}, {"version", 0, nullptr, 'v'}, {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "DvhqAS", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'D':
- printDetail = true;
- break;
- case 'S':
- reportStrategic = true;
- break;
- case 'A':
- solveAgent = true;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'q':
- quiet = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- std::shared_ptr> renderer;
- if (reportStrategic || !game->IsTree()) {
- renderer = MakeMixedStrategyProfileRenderer(std::cout, 0, printDetail);
- }
- else {
- renderer = MakeMixedBehaviorProfileRenderer(std::cout, 0, printDetail);
- }
-
- if (game->IsTree() && solveAgent) {
- EnumPureAgentSolve(game,
- [&](const MixedBehaviorProfile &p) { renderer->Render(p); });
- }
- else {
- EnumPureStrategySolve(game,
- [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/gt/nfggnm.cc b/src/tools/gt/nfggnm.cc
deleted file mode 100644
index d6f72280c7..0000000000
--- a/src/tools/gt/nfggnm.cc
+++ /dev/null
@@ -1,242 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/gt/nfggnm.cc
-// Gambit frontend to Gametracer global Newton method
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/gnm/gnm.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-namespace {
-
-void RenderGNMEvent(const GNMEvent &p_event,
- const std::shared_ptr> &p_renderer)
-{
- std::visit(
- [p_renderer](const Event &event) {
- if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, "pert");
- }
- else if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, "start");
- }
- else if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, lexical_cast(event.lambda));
- }
- },
- p_event);
-}
-
-} // namespace
-
-extern std::vector> ReadStrategyPerturbations(const Game &p_game,
- std::istream &p_stream);
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria using a global Newton method\n";
- p_stream << "Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS show equilibria as floating point with DECIMALS digits\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -n COUNT number of perturbation vectors to generate randomly\n";
- std::cerr << " (mutually exclusive with -s)\n";
- std::cerr << " -R SEED seed the random number generator used to generate\n";
- std::cerr << " perturbation vectors (default is to seed from system\n";
- std::cerr << " entropy); requires -n\n";
- std::cerr << " -s FILE file containing perturbation vectors\n";
- std::cerr << " (mutually exclusive with -n)\n";
- std::cerr << " -m LAMBDA lambda to end tracing (must be negative, default "
- << std::to_string(GNM_LAMBDA_END_DEFAULT) << ")\n";
- std::cerr << " -f FREQ frequency to run local Newton method (default "
- << std::to_string(GNM_LOCAL_NEWTON_INTERVAL_DEFAULT) << ")\n";
- std::cerr << " -i MAXITS maximum iterations in local Newton (default "
- << std::to_string(GNM_LOCAL_NEWTON_MAXITS_DEFAULT) << ")\n";
- std::cerr << " -c STEPS number of steps in each support cell (default "
- << std::to_string(GNM_STEPS_DEFAULT) << ")\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -V, --verbose verbose mode (shows intermediate output)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- bool quiet = false, verbose = false, numVectorsSet = false;
- int numDecimals = 6, numVectors = 1;
- double lambdaEnd = GNM_LAMBDA_END_DEFAULT;
- int localNewtonInterval = GNM_LOCAL_NEWTON_INTERVAL_DEFAULT;
- int localNewtonMaxits = GNM_LOCAL_NEWTON_MAXITS_DEFAULT;
- int steps = GNM_STEPS_DEFAULT;
- std::string startFile;
- std::optional seed;
-
- int long_opt_index = 0;
- option long_options[] = {{"help", 0, nullptr, 'h'},
- {"version", 0, nullptr, 'v'},
- {"verbose", 0, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "d:n:s:m:f:i:c:R:qvVh", long_options, &long_opt_index)) !=
- -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'q':
- quiet = true;
- break;
- case 'V':
- verbose = true;
- break;
- case 'd':
- numDecimals = atoi(optarg);
- break;
- case 'n':
- numVectors = atoi(optarg);
- numVectorsSet = true;
- break;
- case 'R':
- seed = std::strtoul(optarg, nullptr, 10);
- break;
- case 's':
- startFile = optarg;
- break;
- case 'm':
- lambdaEnd = atof(optarg);
- break;
- case 'f':
- localNewtonInterval = atoi(optarg);
- break;
- case 'i':
- localNewtonMaxits = atoi(optarg);
- break;
- case 'c':
- steps = atoi(optarg);
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- if (lambdaEnd >= 0.0) {
- std::cerr << "Error: Value for -m (end lambda) must be negative\n";
- return 1;
- }
- if (localNewtonInterval <= 0) {
- std::cerr << "Error: Value for -f (local Newton frequency) must be at least 1\n";
- return 1;
- }
- if (localNewtonMaxits <= 0) {
- std::cerr << "Error: Value for -i (local Newton iterations) must be at least 1\n";
- return 1;
- }
- if (steps <= 0) {
- std::cerr << "Error: Value for -c (steps in support cell) must be at least 1\n";
- return 1;
- }
- if (numVectorsSet && !startFile.empty()) {
- std::cerr << "Error: The -n and -s options are mutually exclusive.\n";
- return 1;
- }
- if (seed && !numVectorsSet) {
- std::cerr << "Error: The -R option requires -n.\n";
- return 1;
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- return 1;
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- auto renderer = MakeMixedStrategyProfileRenderer(std::cout, numDecimals, false);
- std::vector> perts;
- if (!startFile.empty()) {
- std::ifstream startPerts(startFile.c_str());
- perts = ReadStrategyPerturbations(game, startPerts);
- }
- else {
- // Generate the desired number of points randomly
- auto engine = MakeRandomEngine(seed);
- perts = NewRandomStrategyProfiles(game, numVectors, engine);
- }
- for (auto pert : perts) {
- GNMStrategySolve(
- pert, lambdaEnd, steps, localNewtonInterval, localNewtonMaxits,
- [renderer](const MixedStrategyProfile &p_profile) {
- renderer->Render(p_profile);
- },
- [renderer, verbose](const GNMEvent &p_event) {
- if (verbose) {
- RenderGNMEvent(p_event, renderer);
- }
- });
- }
- return 0;
- }
- catch (std::exception &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/gt/nfggt.cc b/src/tools/gt/nfggt.cc
deleted file mode 100644
index 6a423b28e0..0000000000
--- a/src/tools/gt/nfggt.cc
+++ /dev/null
@@ -1,51 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/gt/nfggt.cc
-// Gambit frontend to Gametracer
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include "games.h"
-
-using namespace Gambit;
-
-std::vector> ReadStrategyPerturbations(const Game &p_game,
- std::istream &p_stream)
-{
- std::vector> profiles;
- while (!p_stream.eof() && !p_stream.bad()) {
- MixedStrategyProfile p(p_game->NewMixedStrategyProfile(0.0));
- if (p_stream.peek() == EOF) {
- break;
- }
- p_stream >> p[1];
- for (size_t i = 2; i <= p.MixedProfileLength(); i++) {
- if (p_stream.eof() || p_stream.bad()) {
- break;
- }
- char comma;
- p_stream >> comma;
- p_stream >> p[i];
- }
- // Read in the rest of the line and discard
- std::string foo;
- std::getline(p_stream, foo);
- profiles.push_back(p);
- }
- return profiles;
-}
diff --git a/src/tools/gt/nfgipa.cc b/src/tools/gt/nfgipa.cc
deleted file mode 100644
index ab52fd6534..0000000000
--- a/src/tools/gt/nfgipa.cc
+++ /dev/null
@@ -1,196 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/gt/nfgipa.cc
-// Gambit frontend to Gametracer IPA
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/ipa/ipa.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-namespace {
-
-void RenderIPAEvent(const IPAEvent &p_event,
- const std::shared_ptr> &p_renderer)
-{
- std::visit(
- [p_renderer](const Event &event) {
- if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, "iter-" + lexical_cast(event.iteration));
- }
- },
- p_event);
-}
-
-} // namespace
-
-extern std::vector> ReadStrategyPerturbations(const Game &p_game,
- std::istream &p_stream);
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria using iterated polymatrix approximation\n";
- p_stream << "Gametracer version 0.2, Copyright (C) 2002, Ben Blum and Christian Shelton\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS show equilibria as floating point with DECIMALS digits\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -n COUNT number of perturbation vectors to generate randomly\n";
- std::cerr << " (mutually exclusive with -s)\n";
- std::cerr << " -R SEED seed the random number generator used to generate\n";
- std::cerr << " perturbation vectors (default is to seed from system\n";
- std::cerr << " entropy); requires -n\n";
- std::cerr << " -s FILE file containing perturbation vectors\n";
- std::cerr << " (mutually exclusive with -n)\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -V, --verbose verbose mode (shows intermediate output)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- bool quiet = false, verbose = false, numVectorsSet = false;
- int numDecimals = 6, numVectors = 1;
- std::string startFile;
- std::optional seed;
-
- int long_opt_index = 0;
- option long_options[] = {{"help", 0, nullptr, 'h'},
- {"version", 0, nullptr, 'v'},
- {"verbose", 0, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "d:n:s:R:vVqh", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'q':
- quiet = true;
- break;
- case 'V':
- verbose = true;
- break;
- case 'd':
- numDecimals = atoi(optarg);
- break;
- case 'n':
- numVectors = atoi(optarg);
- numVectorsSet = true;
- break;
- case 'R':
- seed = std::strtoul(optarg, nullptr, 10);
- break;
- case 's':
- startFile = optarg;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- if (numVectorsSet && !startFile.empty()) {
- std::cerr << "Error: The -n and -s options are mutually exclusive.\n";
- return 1;
- }
- if (seed && !numVectorsSet) {
- std::cerr << "Error: The -R option requires -n.\n";
- return 1;
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- auto renderer = MakeMixedStrategyProfileRenderer(std::cout, numDecimals, false);
-
- std::vector> perts;
- if (!startFile.empty()) {
- std::ifstream startPerts(startFile.c_str());
- perts = ReadStrategyPerturbations(game, startPerts);
- }
- else {
- // Generate the desired number of points randomly
- auto engine = MakeRandomEngine(seed);
- perts = NewRandomStrategyProfiles(game, numVectors, engine);
- }
-
- for (auto pert : perts) {
- IPAStrategySolve(
- pert,
- [renderer](const MixedStrategyProfile &p_profile) {
- renderer->Render(p_profile);
- },
- [renderer, verbose](const IPAEvent &p_event) {
- if (verbose) {
- RenderIPAEvent(p_event, renderer);
- }
- });
- }
- return 0;
- }
- catch (std::exception &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/lcp/lcp.cc b/src/tools/lcp/lcp.cc
deleted file mode 100644
index 37de3ed05e..0000000000
--- a/src/tools/lcp/lcp.cc
+++ /dev/null
@@ -1,188 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/lcp/lcp.cc
-// Compute Nash equilibria via linear complementarity program
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/lcp/lcp.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria by solving a linear complementarity program\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "For an extensive game, computes one equilibrium via the sequence form.\n";
- std::cerr << "For a strategic game (or with -S), with no options, reports all\n";
- std::cerr << "accessible Nash equilibria.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS compute using floating-point arithmetic;\n";
- std::cerr << " display results with DECIMALS digits\n";
- std::cerr << " -S use strategic game\n";
- std::cerr << " -e EQA terminate after finding EQA equilibria\n";
- std::cerr << " (strategic games only; default is to find all\n";
- std::cerr << " accessible equilibria)\n";
- std::cerr << " -r DEPTH terminate recursion at DEPTH\n";
- std::cerr << " (strategic games only; only if number of\n";
- std::cerr << " equilibria sought is not 1)\n";
- std::cerr << " -D print detailed information about equilibria\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- int c;
- bool useFloat = false, useStrategic = false, quiet = false;
- bool printDetail = false;
- int numDecimals = 6, maxDepth = 0;
- std::optional stopAfter;
-
- int long_opt_index = 0;
- option long_options[] = {
- {"help", 0, nullptr, 'h'}, {"version", 0, nullptr, 'v'}, {nullptr, 0, nullptr, 0}};
- while ((c = getopt_long(argc, argv, "d:DvhqSe:r:", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'd':
- useFloat = true;
- numDecimals = atoi(optarg);
- break;
- case 'D':
- printDetail = true;
- break;
- case 'e': {
- size_t parsed;
- const auto *begin = optarg;
- const auto *end = optarg + std::strlen(optarg);
- const auto result = std::from_chars(begin, end, parsed);
- if (result.ec != std::errc{} || result.ptr != end || parsed == 0) {
- std::cerr << "Error: -e argument must be a positive integer; got '" << optarg << "'."
- << std::endl;
- exit(1);
- }
- stopAfter = parsed;
- break;
- }
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'q':
- quiet = true;
- break;
- case 'r':
- maxDepth = atoi(optarg);
- break;
- case 'S':
- useStrategic = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (!game->IsTree() || useStrategic) {
- if (useFloat) {
- auto renderer =
- MakeMixedStrategyProfileRenderer(std::cout, numDecimals, printDetail);
- LcpStrategySolve(
- game, stopAfter, maxDepth,
- [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- }
- else {
- auto renderer =
- MakeMixedStrategyProfileRenderer(std::cout, numDecimals, printDetail);
- LcpStrategySolve(
- game, stopAfter, maxDepth,
- [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- }
- }
- else {
- if (useFloat) {
- auto renderer =
- MakeMixedBehaviorProfileRenderer(std::cout, numDecimals, printDetail);
- LcpBehaviorSolve(
- game, [&](const MixedBehaviorProfile &p) { renderer->Render(p); });
- }
- else {
- auto renderer =
- MakeMixedBehaviorProfileRenderer(std::cout, numDecimals, printDetail);
- LcpBehaviorSolve(
- game, [&](const MixedBehaviorProfile &p) { renderer->Render(p); });
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/liap/liap.cc b/src/tools/liap/liap.cc
deleted file mode 100644
index 5e33b13e47..0000000000
--- a/src/tools/liap/liap.cc
+++ /dev/null
@@ -1,288 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/liap/liap.cc
-// Compute Nash equilibria by minimizing Liapunov function
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/liap/liap.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-template
-void RenderLiapEvent(const Renderer &p_renderer, const LiapEvent &p_event)
-{
- std::visit(
- [&](const EventType &event) {
- if constexpr (std::is_same_v>) {
- p_renderer->Render(event.profile, "start");
- }
- else if constexpr (std::is_same_v>) {
- p_renderer->Render(event.profile, "end");
- }
- },
- p_event);
-}
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria by minimizing the Lyapunov function\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, attempts to compute one equilibrium starting at centroid.\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -A compute agent form equilibria for extensive games\n";
- std::cerr << " -d DECIMALS print probabilities with DECIMALS digits\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -n COUNT number of starting points to generate randomly\n";
- std::cerr << " (mutually exclusive with -s)\n";
- std::cerr << " -R SEED seed the random number generator used to generate\n";
- std::cerr << " starting points (default is to seed from system entropy);\n";
- std::cerr << " requires -n\n";
- std::cerr << " -i MAXITER maximum number of iterations per point (default is 1000)\n";
- std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
- std::cerr << " payoffs in the game\n";
- std::cerr << " -s FILE file containing starting points\n";
- std::cerr << " (mutually exclusive with -n)\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -V, --verbose verbose mode (shows intermediate output)\n";
- std::cerr << " (default is to only show equilibria)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-std::vector> ReadStrategyProfiles(const Game &p_game,
- std::istream &p_stream)
-{
- std::vector> profiles;
- while (!p_stream.eof() && !p_stream.bad()) {
- MixedStrategyProfile p(p_game->NewMixedStrategyProfile(0.0));
- if (p_stream.peek() == EOF) {
- break;
- }
- p_stream >> p[1];
- for (size_t i = 2; i <= p.MixedProfileLength(); i++) {
- if (p_stream.eof() || p_stream.bad()) {
- break;
- }
- char comma;
- p_stream >> comma;
- p_stream >> p[i];
- }
- // Read in the rest of the line and discard
- std::string foo;
- std::getline(p_stream, foo);
- profiles.push_back(p);
- }
- return profiles;
-}
-
-std::vector> ReadBehaviorProfiles(const Game &p_game,
- std::istream &p_stream)
-{
- std::vector> profiles;
- while (!p_stream.eof() && !p_stream.bad()) {
- MixedBehaviorProfile p(p_game);
- if (p_stream.peek() == EOF) {
- break;
- }
- p_stream >> p[1];
- for (size_t i = 2; i <= p.BehaviorProfileLength(); i++) {
- if (p_stream.eof() || p_stream.bad()) {
- break;
- }
- char comma;
- p_stream >> comma;
- p_stream >> p[i];
- }
- // Read in the rest of the line and discard
- std::string foo;
- std::getline(p_stream, foo);
- profiles.push_back(p);
- }
- return profiles;
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- bool quiet = false, solveAgent = false, verbose = false, numTriesSet = false;
- int numTries = 10;
- int maxitsN = 1000;
- int numDecimals = 6;
- double maxregret = 1.0e-4;
- std::string startFile;
- std::optional seed;
-
- int long_opt_index = 0;
- option long_options[] = {{"help", 0, nullptr, 'h'},
- {"version", 0, nullptr, 'v'},
- {"verbose", 0, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "d:n:i:s:m:R:hqVvA", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'd':
- numDecimals = atoi(optarg);
- break;
- case 'n':
- numTries = atoi(optarg);
- numTriesSet = true;
- break;
- case 'R':
- seed = std::strtoul(optarg, nullptr, 10);
- break;
- case 'm':
- maxregret = atof(optarg);
- break;
- case 'i':
- maxitsN = atoi(optarg);
- break;
- case 's':
- startFile = optarg;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'A':
- solveAgent = true;
- break;
- case 'q':
- quiet = true;
- break;
- case 'V':
- verbose = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- if (numTriesSet && !startFile.empty()) {
- std::cerr << "Error: The -n and -s options are mutually exclusive.\n";
- return 1;
- }
- if (seed && !numTriesSet) {
- std::cerr << "Error: The -R option requires -n.\n";
- return 1;
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (!game->IsTree() || !solveAgent) {
- std::vector> starts;
- if (!startFile.empty()) {
- std::ifstream startPoints(startFile.c_str());
- starts = ReadStrategyProfiles(game, startPoints);
- }
- else {
- // Generate the desired number of points randomly
- auto engine = MakeRandomEngine(seed);
- starts = NewRandomStrategyProfiles(game, numTries, engine);
- }
-
- for (const auto &start : starts) {
- auto renderer = MakeMixedStrategyProfileRenderer(std::cout, numDecimals, false);
- LiapStrategySolve(
- start, maxregret, maxitsN,
- [renderer](const MixedStrategyProfile &p_profile) {
- renderer->Render(p_profile);
- },
- [renderer, verbose](const LiapEvent> &event) {
- if (verbose) {
- RenderLiapEvent(renderer, event);
- }
- });
- }
- }
- else {
- std::vector> starts;
- if (!startFile.empty()) {
- std::ifstream startPoints(startFile.c_str());
- starts = ReadBehaviorProfiles(game, startPoints);
- }
- else {
- // Generate the desired number of points randomly
- auto engine = MakeRandomEngine(seed);
- starts = NewRandomBehaviorProfiles(game, numTries, engine);
- }
-
- for (const auto &start : starts) {
- auto renderer = MakeMixedBehaviorProfileRenderer(std::cout, numDecimals, false);
- LiapAgentSolve(
- start, maxregret, maxitsN,
- [renderer](const MixedBehaviorProfile &p_profile) {
- renderer->Render(p_profile);
- },
- [renderer, verbose](const LiapEvent> &event) {
- if (verbose) {
- RenderLiapEvent(renderer, event);
- }
- });
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/logit/logit.cc b/src/tools/logit/logit.cc
deleted file mode 100644
index 86ee2dad5f..0000000000
--- a/src/tools/logit/logit.cc
+++ /dev/null
@@ -1,270 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/logit/logit.cc
-// Command-line driver program for quantal response equilibrium tracing and
-// maximum likelihood estimation.
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include "games.h"
-#include "solvers/logit/logit.h"
-
-using namespace Gambit;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute a branch of the logit equilibrium correspondence\n";
- p_stream << "Gambit version " VERSION ", ";
- p_stream << "Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS show equilibria as floating point with DECIMALS digits\n";
- std::cerr << " -S use strategic game\n";
- std::cerr << " -s STEP initial stepsize (default is .03)\n";
- std::cerr << " -a ACCEL maximum acceleration (default is 1.1)\n";
- std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
- std::cerr << " payoffs in the game\n";
- std::cerr << " -l LAMBDA compute QRE at `lambda` accurately\n";
- std::cerr << " -L FILE compute maximum likelihood estimates;\n";
- std::cerr << " read strategy frequencies from FILE\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -e print only the terminal equilibrium\n";
- std::cerr << " (default is to print the entire branch)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-//
-// Read in a comma-separated values list of observed data values
-//
-bool ReadProfile(std::istream &p_stream, MixedStrategyProfile &p_profile)
-{
- for (size_t i = 1; i <= p_profile.MixedProfileLength(); i++) {
- p_stream >> p_profile[i];
- if (!p_stream) {
- return false;
- }
- if (i < p_profile.MixedProfileLength()) {
- char comma;
- p_stream >> comma;
- if (!p_stream) {
- return false;
- }
- }
- }
- // Read in the rest of the line and discard
- std::string foo;
- std::getline(p_stream, foo);
- return true;
-}
-
-template
-void PrintProfile(std::ostream &p_stream, int p_decimals, const T &p_profile, bool p_nash = false)
-{
- if (p_nash) {
- p_stream << "NE";
- }
- else {
- p_stream.setf(std::ios::fixed);
- p_stream << std::setprecision(p_decimals) << p_profile.GetLambda();
- p_stream.unsetf(std::ios::fixed);
- }
- for (size_t i = 1; i <= p_profile.size(); i++) {
- p_stream << "," << std::setprecision(p_decimals) << p_profile[i];
- }
- if (p_profile.GetLogLike() <= 0.0) {
- p_stream.setf(std::ios::fixed);
- p_stream << "," << std::setprecision(p_decimals) << p_profile.GetLogLike();
- p_stream.unsetf(std::ios::fixed);
- }
- p_stream << std::endl;
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
-
- bool quiet = false, useStrategic = false;
- const double maxLambda = 1000000.0;
- double maxregret = 1.0e-8;
- std::string mleFile;
- double maxDecel = 1.1;
- double hStart = 0.03;
- std::list targetLambda;
- bool fullGraph = true;
- int decimals = 6;
-
- int long_opt_index = 0;
- option long_options[] = {
- {"help", 0, nullptr, 'h'}, {"version", 0, nullptr, 'v'}, {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "d:s:a:m:vqehSL:l:", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'q':
- quiet = true;
- break;
- case 'd':
- decimals = atoi(optarg);
- break;
- case 's':
- hStart = atof(optarg);
- break;
- case 'a':
- maxDecel = atof(optarg);
- break;
- case 'm':
- maxregret = atof(optarg);
- break;
- case 'e':
- fullGraph = false;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'S':
- useStrategic = true;
- break;
- case 'L':
- mleFile = optarg;
- break;
- case 'l':
- targetLambda.push_back(atof(optarg));
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (!game->IsPerfectRecall()) {
- throw UndefinedException(
- "Computing equilibria of games with imperfect recall is not supported.");
- }
-
- if (!mleFile.empty() && (!game->IsTree() || useStrategic)) {
- MixedStrategyProfile frequencies(game->NewMixedStrategyProfile(0.0));
- std::ifstream mleData(mleFile.c_str());
- if (!ReadProfile(mleData, frequencies)) {
- throw std::runtime_error("Error reading strategy frequencies from '" + mleFile + "'.");
- }
-
- auto printer = [fullGraph,
- decimals](const LogitEvent &p_event) {
- if (fullGraph) {
- PrintProfile(std::cout, decimals,
- std::get>(p_event).qre);
- }
- };
- auto result =
- LogitStrategyEstimate(frequencies, maxLambda, 1.0, false, hStart, maxDecel, printer);
- PrintProfile(std::cout, decimals, result);
- return 0;
- }
-
- if (!game->IsTree() || useStrategic) {
- auto printer = [fullGraph,
- decimals](const LogitEvent &p_event) {
- if (fullGraph) {
- PrintProfile(std::cout, decimals,
- std::get>(p_event).qre);
- }
- };
- const LogitQREMixedStrategyProfile start(game);
- if (!targetLambda.empty()) {
- auto result =
- LogitStrategySolveLambda(start, targetLambda, 1.0, hStart, maxDecel, printer);
- for (auto &profile : result) {
- PrintProfile(std::cout, decimals, profile);
- }
- }
- else {
- auto result = LogitStrategySolve(start, maxregret, 1.0, hStart, maxDecel,
- Nash::NullStrategyCallback, printer);
- PrintProfile(std::cout, decimals, result.back(), true);
- }
- }
- else {
- auto printer = [fullGraph,
- decimals](const LogitEvent &p_event) {
- if (fullGraph) {
- PrintProfile(std::cout, decimals,
- std::get>(p_event).qre);
- }
- };
- const LogitQREMixedBehaviorProfile start(game);
- if (!targetLambda.empty()) {
- auto result =
- LogitBehaviorSolveLambda(start, targetLambda, 1.0, hStart, maxDecel, printer);
- for (auto &profile : result) {
- PrintProfile(std::cout, decimals, profile);
- }
- }
- else {
- auto result = LogitBehaviorSolve(start, maxregret, 1.0, hStart, maxDecel,
- Nash::NullBehaviorCallback, printer);
- PrintProfile(std::cout, decimals, result.back(), true);
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/lp/lp.cc b/src/tools/lp/lp.cc
deleted file mode 100644
index 1f85fc3de2..0000000000
--- a/src/tools/lp/lp.cc
+++ /dev/null
@@ -1,158 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/lp/lp.cc
-// Implementation of algorithm to compute mixed strategy equilibria
-// of constant sum normal form games via linear programming
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/lp/lp.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria by solving a linear program\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, computes one Nash equilibrium.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -d DECIMALS compute using floating-point arithmetic;\n";
- std::cerr << " display results with DECIMALS digits\n";
- std::cerr << " -S use strategic game\n";
- std::cerr << " -D print detailed information about equilibria\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- int c;
- int numDecimals = 6;
- bool useFloat = false, useStrategic = false, quiet = false, printDetail = false;
-
- int long_opt_index = 0;
- option long_options[] = {
- {"help", 0, nullptr, 'h'}, {"version", 0, nullptr, 'v'}, {nullptr, 0, nullptr, 0}};
- while ((c = getopt_long(argc, argv, "d:DvqhS", long_options, &long_opt_index)) != -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'd':
- useFloat = true;
- numDecimals = atoi(optarg);
- break;
- case 'D':
- printDetail = true;
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'q':
- quiet = true;
- break;
- case 'S':
- useStrategic = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- if (!game->IsTree() || useStrategic) {
- if (useFloat) {
- auto renderer =
- MakeMixedStrategyProfileRenderer(std::cout, numDecimals, printDetail);
- LpStrategySolve(
- game, [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- }
- else {
- auto renderer =
- MakeMixedStrategyProfileRenderer(std::cout, numDecimals, printDetail);
- LpStrategySolve(
- game, [&](const MixedStrategyProfile &p) { renderer->Render(p); });
- }
- }
- else {
- if (useFloat) {
- auto renderer =
- MakeMixedBehaviorProfileRenderer(std::cout, numDecimals, printDetail);
- LpBehaviorSolve(
- game, [&](const MixedBehaviorProfile &p) { renderer->Render(p); });
- }
- else {
- auto renderer =
- MakeMixedBehaviorProfileRenderer(std::cout, numDecimals, printDetail);
- LpBehaviorSolve(
- game, [&](const MixedBehaviorProfile &p) { renderer->Render(p); });
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/simpdiv/nfgsimpdiv.cc b/src/tools/simpdiv/nfgsimpdiv.cc
deleted file mode 100644
index 4408b8ce2f..0000000000
--- a/src/tools/simpdiv/nfgsimpdiv.cc
+++ /dev/null
@@ -1,277 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/simpdiv/nfgsimpdiv.cc
-// Compute Nash equilibria via simplicial subdivision on the normal form
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#include
-#include
-#include
-#include
-#include "games.h"
-#include "tools/util.h"
-#include "solvers/simpdiv/simpdiv.h"
-
-using namespace Gambit;
-using namespace Gambit::Nash;
-
-template
-void RenderSimpdivEvent(const Renderer &p_renderer, const SimpdivEvent &p_event)
-{
- std::visit(
- [&](const EventType &event) {
- if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, "start");
- }
- else if constexpr (std::is_same_v) {
- p_renderer->Render(event.profile, std::to_string(event.gridSize));
- }
- },
- p_event);
-}
-
-std::vector> ReadProfiles(const Game &p_game,
- std::istream &p_stream)
-{
- std::vector> profiles;
- while (!p_stream.eof() && !p_stream.bad()) {
- MixedStrategyProfile p(p_game->NewMixedStrategyProfile(Rational(0)));
- p_stream >> p[1];
- if (!p_stream) {
- break;
- }
- for (size_t i = 2; i <= p.MixedProfileLength(); i++) {
- if (p_stream.eof() || p_stream.bad()) {
- break;
- }
- char comma;
- p_stream >> comma;
- p_stream >> p[i];
- }
- // Read in the rest of the line and discard
- std::string foo;
- std::getline(p_stream, foo);
- profiles.push_back(p);
- }
- return profiles;
-}
-
-class MixedStrategyCSVAsFloatRenderer final : public MixedStrategyProfileRenderer {
-public:
- explicit MixedStrategyCSVAsFloatRenderer(std::ostream &p_stream, int p_numDecimals = 6)
- : m_stream(p_stream), m_numDecimals(p_numDecimals)
- {
- }
- ~MixedStrategyCSVAsFloatRenderer() override = default;
- void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const override;
-
-private:
- std::ostream &m_stream;
- int m_numDecimals;
-};
-
-void MixedStrategyCSVAsFloatRenderer::Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label) const
-{
- m_stream << p_label;
- for (size_t i = 1; i <= p_profile.MixedProfileLength(); i++) {
- m_stream << "," << lexical_cast(double(p_profile[i]), m_numDecimals);
- }
- m_stream << std::endl;
-}
-
-void PrintBanner(std::ostream &p_stream)
-{
- p_stream << "Compute Nash equilibria using simplicial subdivision\n";
- p_stream << "Gambit version " VERSION ", Copyright (C) 1994-2026, The Gambit Project\n";
- p_stream << "This is free software, distributed under the GNU GPL\n\n";
-}
-
-void PrintHelp(char *progname)
-{
- PrintBanner(std::cerr);
- std::cerr << "Usage: " << progname << " [OPTIONS] [file]\n";
- std::cerr << "If file is not specified, attempts to read game from standard input.\n";
- std::cerr << "With no options, computes one approximate Nash equilibrium.\n\n";
-
- std::cerr << "Options:\n";
- std::cerr << " -g MULT granularity of grid refinement at each step (default is 2)\n";
- std::cerr << " -h, --help print this help message\n";
- std::cerr << " -r DENOM generate random starting points with denominator DENOM\n";
- std::cerr << " (mutually exclusive with -s)\n";
- std::cerr << " -n COUNT number of starting points to generate (requires -r)\n";
- std::cerr << " -R SEED seed the random number generator used to generate\n";
- std::cerr << " starting points (default is to seed from system entropy);\n";
- std::cerr << " requires -n\n";
- std::cerr << " -s FILE file containing starting points\n";
- std::cerr << " (mutually exclusive with -r)\n";
- std::cerr << " -d DECIMALS show profiles as floating point with DECIMALS digits\n";
- std::cerr << " (default is to display rational numbers)\n";
- std::cerr << " -m MAXREGRET maximum regret acceptable as a proportion of range of\n";
- std::cerr << " payoffs in the game (default is 1e-7)\n";
- std::cerr << " -q quiet mode (suppresses banner)\n";
- std::cerr << " -V, --verbose verbose mode (shows intermediate output)\n";
- std::cerr << " (default is to only show equilibria)\n";
- std::cerr << " -v, --version print version information\n";
- exit(0);
-}
-
-int main(int argc, char *argv[])
-{
- opterr = 0;
- std::string startFile;
- bool useRandom = false, numTriesSet = false;
- int randDenom = 1, gridResize = 2, stopAfter = 1, decimals = 0;
- bool verbose = false, quiet = false;
- Rational maxregret(1, 10000000);
- std::optional seed;
-
- int long_opt_index = 0;
- struct option long_options[] = {{"help", 0, nullptr, 'h'},
- {"version", 0, nullptr, 'v'},
- {"verbose", 0, nullptr, 'V'},
- {nullptr, 0, nullptr, 0}};
- int c;
- while ((c = getopt_long(argc, argv, "g:hVvn:r:s:m:d:R:q", long_options, &long_opt_index)) !=
- -1) {
- switch (c) {
- case 'v':
- PrintBanner(std::cerr);
- exit(0);
- case 'g':
- gridResize = atoi(optarg);
- break;
- case 'h':
- PrintHelp(argv[0]);
- break;
- case 'r':
- useRandom = true;
- randDenom = atoi(optarg);
- break;
- case 'n':
- stopAfter = atoi(optarg);
- numTriesSet = true;
- break;
- case 'R':
- seed = std::strtoul(optarg, nullptr, 10);
- break;
- case 'm':
- maxregret = lexical_cast(std::string(optarg));
- break;
- case 'd':
- decimals = atoi(optarg);
- break;
- case 's':
- startFile = optarg;
- break;
- case 'q':
- quiet = true;
- break;
- case 'V':
- verbose = true;
- break;
- case '?':
- if (isprint(optopt)) {
- std::cerr << argv[0] << ": Unknown option `-" << static_cast(optopt) << "'.\n";
- }
- else {
- std::cerr << argv[0] << ": Unknown option character `\\x" << optopt << "`.\n";
- }
- return 1;
- default:
- abort();
- }
- }
-
- if (!quiet) {
- PrintBanner(std::cerr);
- }
-
- if (useRandom && !startFile.empty()) {
- std::cerr << "Error: The -r and -s options are mutually exclusive.\n";
- return 1;
- }
- if (numTriesSet && !useRandom) {
- std::cerr << "Error: The -n option requires -r.\n";
- return 1;
- }
- if (seed && !numTriesSet) {
- std::cerr << "Error: The -R option requires -n.\n";
- return 1;
- }
-
- std::istream *input_stream = &std::cin;
- std::ifstream file_stream;
- if (optind < argc) {
- file_stream.open(argv[optind]);
- if (!file_stream.is_open()) {
- std::ostringstream error_message;
- error_message << argv[0] << ": " << argv[optind];
- perror(error_message.str().c_str());
- exit(1);
- }
- input_stream = &file_stream;
- }
-
- try {
- const Game game = ReadGame(*input_stream);
- std::vector> starts;
- if (!startFile.empty()) {
- std::ifstream startPoints(startFile.c_str());
- starts = ReadProfiles(game, startPoints);
- }
- else if (useRandom) {
- auto engine = MakeRandomEngine(seed);
- starts = NewRandomStrategyProfiles(game, stopAfter, randDenom, engine);
- }
- else {
- starts.push_back(SimpdivDefaultStart(game));
- }
- for (auto start : starts) {
- if (decimals > 0) {
- auto renderer = std::make_shared(std::cout, decimals);
- SimpdivStrategySolve(
- start, maxregret, gridResize, 0,
- [&](const MixedStrategyProfile &p) { renderer->Render(p); },
- [&](const SimpdivEvent &event) {
- if (verbose) {
- RenderSimpdivEvent(renderer, event);
- }
- });
- }
- else {
- auto renderer = std::make_shared>(std::cout);
- SimpdivStrategySolve(
- start, maxregret, gridResize, 0,
- [&](const MixedStrategyProfile &p) { renderer->Render(p); },
- [&](const SimpdivEvent &event) {
- if (verbose) {
- RenderSimpdivEvent(renderer, event);
- }
- });
- }
- }
- return 0;
- }
- catch (std::runtime_error &e) {
- std::cerr << "Error: " << e.what() << std::endl;
- return 1;
- }
-}
diff --git a/src/tools/util.h b/src/tools/util.h
deleted file mode 100644
index f027297da8..0000000000
--- a/src/tools/util.h
+++ /dev/null
@@ -1,293 +0,0 @@
-//
-// This file is part of Gambit
-// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org)
-//
-// FILE: src/tools/util.h
-// Utility functions common to command-line tools
-//
-// This program is free software; you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation; either version 2 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program; if not, write to the Free Software
-// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
-//
-
-#ifndef TOOLS_UTIL_H
-#define TOOLS_UTIL_H
-
-#include
-#include
-#include "games/stratmixed.h"
-#include "games/behavmixed.h"
-
-namespace Gambit {
-
-/// @brief Constructs a random engine for generating starting points for command-line tools.
-/// If p_seed is given, the engine is seeded deterministically with it, so that the run
-/// can be reproduced; otherwise, the engine is seeded from system entropy.
-inline std::default_random_engine MakeRandomEngine(std::optional p_seed)
-{
- if (p_seed) {
- return std::default_random_engine(*p_seed);
- }
- std::random_device rd;
- return std::default_random_engine(rd());
-}
-
-template class MixedProfileRenderer {
-public:
- virtual ~MixedProfileRenderer() = default;
- virtual void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const = 0;
- virtual void Render(const MixedBehaviorProfile &p_profile,
- const std::string &p_label = "NE") const = 0;
-};
-
-//
-// Encapsulates the rendering of a mixed strategy profile to various text formats.
-// Implements automatic conversion of behavior strategy profiles to mixed
-// strategy profiles.
-//
-template class MixedStrategyProfileRenderer : public MixedProfileRenderer {
-public:
- ~MixedStrategyProfileRenderer() override = default;
- void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const override = 0;
- void Render(const MixedBehaviorProfile &p_profile,
- const std::string &p_label = "NE") const override
- {
- Render(p_profile.ToMixedProfile(), p_label);
- }
-};
-
-template
-class MixedStrategyProfileCSVRenderer final : public MixedStrategyProfileRenderer {
-public:
- explicit MixedStrategyProfileCSVRenderer(std::ostream &p_stream, int p_numDecimals = 6)
- : m_stream(p_stream), m_numDecimals(p_numDecimals)
- {
- }
- ~MixedStrategyProfileCSVRenderer() override = default;
- void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const override;
-
-private:
- std::ostream &m_stream;
- int m_numDecimals;
-};
-
-template
-class MixedStrategyProfileDetailRenderer final : public MixedStrategyProfileRenderer {
-public:
- explicit MixedStrategyProfileDetailRenderer(std::ostream &p_stream, int p_numDecimals = 6)
- : m_stream(p_stream), m_numDecimals(p_numDecimals)
- {
- }
- ~MixedStrategyProfileDetailRenderer() override = default;
- void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const override;
-
-private:
- std::ostream &m_stream;
- int m_numDecimals;
-};
-
-template
-std::shared_ptr>
-MakeMixedStrategyProfileRenderer(std::ostream &p_stream, int p_numDecimals, bool p_printDetail)
-{
- if (p_printDetail) {
- return std::make_shared>(p_stream, p_numDecimals);
- }
- return std::make_shared>(p_stream, p_numDecimals);
-}
-
-//
-// Encapsulates the rendering of a mixed behavior profile to various text formats.
-//
-template class MixedBehaviorProfileRenderer : public MixedProfileRenderer {
-public:
- ~MixedBehaviorProfileRenderer() override = default;
- void Render(const MixedStrategyProfile &p_profile,
- const std::string &p_label = "NE") const override
- {
- Render(MixedBehaviorProfile(p_profile), p_label);
- }
- void Render(const MixedBehaviorProfile &p_profile,
- const std::string &p_label = "NE") const override = 0;
-};
-
-template
-class MixedBehaviorProfileCSVRenderer final : public MixedBehaviorProfileRenderer {
-public:
- explicit MixedBehaviorProfileCSVRenderer(std::ostream &p_stream, int p_numDecimals = 6)
- : m_stream(p_stream), m_numDecimals(p_numDecimals)
- {
- }
- ~MixedBehaviorProfileCSVRenderer() override = default;
- void Render(const MixedBehaviorProfile &p_profile,
- const std::string &p_label = "NE") const override;
-
-private:
- std::ostream &m_stream;
- int m_numDecimals;
-};
-
-template
-class MixedBehaviorProfileDetailRenderer final : public MixedBehaviorProfileRenderer {
-public:
- explicit MixedBehaviorProfileDetailRenderer(std::ostream &p_stream, int p_numDecimals = 6)
- : m_stream(p_stream), m_numDecimals(p_numDecimals)
- {
- }
- ~MixedBehaviorProfileDetailRenderer() override = default;
- void Render(const MixedBehaviorProfile &p_profile,
- const std::string &p_label = "NE") const override;
-
-private:
- std::ostream &m_stream;
- int m_numDecimals;
-};
-
-template
-std::shared_ptr