diff --git a/ChangeLog b/ChangeLog
index b12b97ddca..3ed43f880a 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -5,12 +5,19 @@
recently opened games. (#1108)
- Details on bifurcations found and actions taken to resolve them (by perturbation) are now reported
by `nash.logit_solve` (and related functions) (#186)
+- `-D`/`--detail` is now available on every `gambit-*` command-line tool that reports equilibria.
+- The command-line tools' `-D` report for behavior profiles now includes the probability
+ distribution over full histories (discussion #1094)
### Changed
- Refined the perturbation approach in `path.cc` (logit and hp solvers) to better handle paths on games
with bifurcations. (#492)
- Nash solver algorithms now return a method-specific `Result` class, rather than a generic class,
with potentially richer information about the performance of the algorithm.
+- The command-line tools' `-D` (detail) report for behavior profiles now identifies each node by its
+ `History` (the actions from the root, comma-joined).
+- The command-line tools' `-D` report now identifies each player by their own label, rather than by
+ their position among the game's players.
## [17.0.0-beta.1] - 2026-09-08
diff --git a/doc/tools.convert.rst b/doc/tools.convert.rst
deleted file mode 100644
index 2273bddc8d..0000000000
--- a/doc/tools.convert.rst
+++ /dev/null
@@ -1,65 +0,0 @@
-:program:`gambit-convert`
-=========================
-
-:program:`gambit-convert` reads a game on standard input in any supported format
-and converts it to another text representation. Currently, this tool supports
-outputting the strategic form of the game in one of these formats:
-
-* A standard HTML table.
-* A LaTeX fragment in the format of Martin Osborne's `sgame` macros
- (see http://www.economics.utoronto.ca/osborne/latex/index.html).
-
-
-.. program:: gambit-convert
-
-.. cmdoption:: -O FORMAT
-
- Required. Specifies the output format. Supported options for
- `FORMAT` are `html` or `sgame`.
-
-.. cmdoption:: -r PLAYER
-
- Specifies the player number to place on the rows of the tables.
- The default if not specified is to place player 1 on the rows.
-
-.. cmdoption:: -c PLAYER
-
- Specifies the player number to place on the columns of the tables.
- The default if not specified is to place player 2 on the columns.
-
-.. cmdoption:: -h
-
- Prints a help message listing the available options.
-
-.. cmdoption:: -q
-
- Suppresses printing of the banner at program launch.
-
-
-Example invocation for HTML output::
-
- $ gambit-convert -O html 2x2.nfg
- Convert games among various file formats
- Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project
- This is free software, distributed under the GNU GPL
-
-
Two person 2 x 2 game with unique mixed equilibrium
-
-
-
-Example invocation for LaTeX output::
-
- $ gambit-convert -O sgame 2x2.nfg
- Convert games among various file formats
- Gambit version |release|, Copyright (C) 1994-2026, The Gambit Project
- This is free software, distributed under the GNU GPL
-
- \begin{game}{2}{2}[Player 1][Player 2]
- &1 & 2\\
- 1 & $2,0$ & $0,1$ \\
- 2 & $0,1$ & $1,0$
- \end{game}
diff --git a/doc/tools.enummixed.rst b/doc/tools.enummixed.rst
index 76d87cdfeb..2319c134b8 100644
--- a/doc/tools.enummixed.rst
+++ b/doc/tools.enummixed.rst
@@ -29,6 +29,17 @@ See the :ref:`algorithm description ` for full details.
each convex set, prefixed by convex-N , where N indexes the set. The
set of all equilibria, then, is the union of these convex sets.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed (and, with :option:`-c`, on each member of every
+ convex set reported).
+
.. cmdoption:: -h
Prints a help message listing the available options.
diff --git a/doc/tools.enumpoly.rst b/doc/tools.enumpoly.rst
index d7d27fbf6a..b425a380c0 100644
--- a/doc/tools.enumpoly.rst
+++ b/doc/tools.enumpoly.rst
@@ -50,6 +50,16 @@ and (in verbose mode) the support is identified with the label
strategies for extensive games. (This has no effect for strategic
games, since a strategic game is its own reduced strategic game.)
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy or action probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed.
+
.. cmdoption:: -m
.. versionadded:: 16.3.0
diff --git a/doc/tools.gnm.rst b/doc/tools.gnm.rst
index c92fd2534d..1716a22919 100644
--- a/doc/tools.gnm.rst
+++ b/doc/tools.gnm.rst
@@ -19,6 +19,16 @@ subsets of equilibria being found.
Express all output using decimal representations
with the specified number of digits.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed.
+
.. cmdoption:: -h
Prints a help message listing the available options.
diff --git a/doc/tools.hp.rst b/doc/tools.hp.rst
index b3a00b7d7a..d730357e9e 100644
--- a/doc/tools.hp.rst
+++ b/doc/tools.hp.rst
@@ -20,6 +20,16 @@ different equilibria being found.
Express all output using decimal representations
with the specified number of digits.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed.
+
.. cmdoption:: -h
Prints a help message listing the available options.
diff --git a/doc/tools.ipa.rst b/doc/tools.ipa.rst
index d2f83f391f..8fb6a9168f 100644
--- a/doc/tools.ipa.rst
+++ b/doc/tools.ipa.rst
@@ -19,6 +19,16 @@ equilibria being found.
Express all output using decimal representations
with the specified number of digits.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed.
+
.. cmdoption:: -h
Prints a help message listing the available options.
diff --git a/doc/tools.liap.rst b/doc/tools.liap.rst
index 78ade88c4b..62575ca6d0 100644
--- a/doc/tools.liap.rst
+++ b/doc/tools.liap.rst
@@ -47,6 +47,16 @@ See the :ref:`algorithm description ` for full details.
Express all output using decimal representations with the
specified number of digits.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy or action probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed.
+
.. cmdoption:: -n
Specify the number of starting points to randomly generate.
@@ -88,13 +98,17 @@ See the :ref:`algorithm description ` for full details.
output of equilibria (excluding the initial NE tag).
Mutually exclusive with :option:`-n`.
-.. cmdoption:: -v
+.. cmdoption:: -V, --verbose
Sets verbose mode. In verbose mode, initial points, as well as
points at which the minimization fails at a constrained local minimum
that is not a Nash equilibrium, are all output, in addition to any
equilibria found.
+.. cmdoption:: -v, --version
+
+ Prints version information and exits.
+
Computing an equilibrium in mixed strategies of the example in Figure 2 of :cite:p:`Sel75`::
$ gambit-liap catalog/games/journals/ijgt/selten1975/fig2.efg
diff --git a/doc/tools.logit.rst b/doc/tools.logit.rst
index 2b709b7c39..521b7bef4f 100644
--- a/doc/tools.logit.rst
+++ b/doc/tools.logit.rst
@@ -74,6 +74,18 @@ See the :ref:`algorithm description ` for full details.
this switch is specified, only the approximation to the Nash
equilibrium at the end of the branch is output.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ If tracing reaches a point accepted as a Nash equilibrium (per
+ :option:`-m`), print detailed, human-readable information about that
+ terminal profile instead of the usual comma-separated line -- whether
+ or not :option:`-e` was also given, since both end with the same
+ accepted-equilibrium line. This has no effect on the points printed
+ along the way there, or on the output of :option:`-l` or of maximum-likelihood
+ estimation, neither of which is itself an accepted equilibrium.
+
.. cmdoption:: -b
.. versionadded:: 17.0.0
diff --git a/doc/tools.rst b/doc/tools.rst
index 99445adf14..856a67c32c 100644
--- a/doc/tools.rst
+++ b/doc/tools.rst
@@ -30,9 +30,12 @@ strategy. For behavior strategy profiles, the probabilities are sorted
by player, then information set, then action number, where the
information sets for a player are sorted by the order in which they
are encountered in a depth-first traversal of the game tree.
-Many programs take an option `-D`, which, if specified, instead prints
+Every program takes an option `-D`, which, if specified, instead prints
a more verbose, human-friendly description of each strategy profile
-computed.
+computed. (`gambit-logit` is a partial exception: since it traces a whole
+branch rather than reporting discrete equilibria, `-D` there affects only
+the terminal point, if tracing reaches one accepted as a Nash equilibrium --
+see its own documentation.)
Many of the programs optionally output additional information
about the operation of the algorithm. These outputs have other,
diff --git a/doc/tools.simpdiv.rst b/doc/tools.simpdiv.rst
index 14a7a1a33b..bd8bc2b224 100644
--- a/doc/tools.simpdiv.rst
+++ b/doc/tools.simpdiv.rst
@@ -83,6 +83,16 @@ options to specify additional starting points for the algorithm.
this option sacrifices some precision in reporting the output of the method, in exchange for
probabilities which are more human-readable.
+.. cmdoption:: -D
+
+ .. versionadded:: 17.0.0
+
+ The default output format for computed equilibria is a
+ comma-separated list of strategy probabilities, suitable
+ for postprocessing by automated tools. Specifying `-D` instead
+ causes the program to output greater detail on each equilibrium
+ profile computed, respecting :option:`-d` the same way.
+
.. cmdoption:: -V, --verbose
Sets verbose mode. In verbose mode, initial points, as well as
diff --git a/src/pygambit/cli/common.py b/src/pygambit/cli/common.py
index e667e56de2..329d60ae7b 100644
--- a/src/pygambit/cli/common.py
+++ b/src/pygambit/cli/common.py
@@ -23,6 +23,7 @@
from __future__ import annotations
+import csv
import functools
import io
import pathlib
@@ -32,7 +33,6 @@
import numpy as np
import pygambit as gbt
-from pygambit.gambit import _history_of
_GAME_FORMATS = (
("NFG", gbt.read_nfg),
@@ -266,52 +266,60 @@ def render_support_csv(
def render_profile_detail(
profile: gbt.MixedStrategyProfile | gbt.MixedBehaviorProfile,
decimals: int,
+ fixed: bool = True,
+ as_float: bool = False,
) -> str:
"""Render a strategy or behavior profile as a human-readable description,
matching `MixedStrategyProfileDetailRenderer` and
- `MixedBehaviorProfileDetailRenderer` in the C++ tools.
+ `MixedBehaviorProfileDetailRenderer` in the C++ tools. See `format_value` for
+ the meaning of `fixed` and `as_float`.
"""
if _is_behavior_profile(profile):
- return _render_behavior_detail(profile, decimals)
- return _render_strategy_detail(profile, decimals)
+ return _render_behavior_detail(profile, decimals, fixed, as_float)
+ return _render_strategy_detail(profile, decimals, fixed, as_float)
-def _name_or_number(node: gbt.Node) -> str:
- # Gambit's Python API numbers nodes from 0; the C++ tools display the
- # underlying (1-based) engine numbering. `Node._label`/`._number` are private
- # (not part of the public API) but still Python-accessible, same as
- # `Game._get_infosets` just below -- rendering needs the node's own label/engine
- # number, which a History alone cannot provide.
- return node._label if node._label else str(node._number() + 1)
+def _render_history(actions: tuple[str, ...]) -> str:
+ # Comma-joining alone is ambiguous: action labels may themselves contain commas
+ # (or quotes), so e.g. actions "A,B" then "C" and "A" then "B,C" would both render
+ # as "A,B,C". CSV quote-when-needed is a standard, already-correct fix for exactly
+ # this problem.
+ buf = io.StringIO()
+ csv.writer(buf, lineterminator="").writerow(actions)
+ return buf.getvalue() or "(root)"
-def _render_strategy_detail(profile: gbt.MixedStrategyProfile, decimals: int) -> str:
+def _render_strategy_detail(
+ profile: gbt.MixedStrategyProfile, decimals: int, fixed: bool = True, as_float: bool = False
+) -> str:
lines = []
- for number, player in enumerate(profile.game.players, start=1):
- lines.append(f"Strategy profile for player {number}:")
+ for player in profile.game.players:
+ lines.append(f"Strategy profile for {player}:")
lines.append("Strategy Prob Value")
lines.append("-------- ----------- -----------")
probs = profile[player]
values = profile.strategy_values[player]
for strategy in profile.game.get_strategies(player):
- prob = format_value(probs[strategy], decimals)
- value = format_value(values[strategy], decimals)
+ prob = format_value(probs[strategy], decimals, fixed, as_float)
+ value = format_value(values[strategy], decimals, fixed, as_float)
lines.append(f"{strategy:>8} {prob:>10} {value:>11}")
return "\n".join(lines)
-def _render_behavior_detail(profile: gbt.MixedBehaviorProfile, decimals: int) -> str:
+def _render_behavior_detail(
+ profile: gbt.MixedBehaviorProfile, decimals: int, fixed: bool = True, as_float: bool = False
+) -> str:
lines = []
action_values = profile.action_values
beliefs = profile.beliefs
realiz_probs = profile.realiz_probs
- for number, player in enumerate(profile.game.players, start=1):
- lines.append(f"Behavior profile for player {number}:")
+ for player in profile.game.players:
+ lines.append(f"Behavior profile for {player}:")
lines.append("Infoset Action Prob Value")
lines.append("------- ------- ----------- -----------")
- # Numbered by position among the player's information sets: Infoset no
- # longer exists as an object, so there is currently no infoset-level label
- # to prefer over this, unlike _name_or_number's use for players/nodes/actions.
+ # Numbered by position among the player's information sets -- a compact key
+ # matched against the same numbering in the History table below, rather than
+ # a description; see that table's own comment.
for infoset_number, (history, (_, mixed_action)) in enumerate(
zip(profile.game.get_infosets(player), profile[player], strict=True), start=1
):
@@ -320,32 +328,52 @@ def _render_behavior_detail(profile: gbt.MixedBehaviorProfile, decimals: int) ->
for action in profile.game.get_actions(selector):
prob = mixed_action[action]
value = values[action]
- value_text = format_value(value, decimals) if value is not None else ""
+ value_text = (
+ format_value(value, decimals, fixed, as_float) if value is not None else ""
+ )
lines.append(
f"{infoset_number:>7} {action:>7} "
- f"{format_value(prob, decimals):>11} {value_text:>11}"
+ f"{format_value(prob, decimals, fixed, as_float):>11} {value_text:>11}"
)
lines.append("")
- lines.append("Infoset Node Belief Prob")
- lines.append("------- ------- ----------- -----------")
- # Uses the private, Node-returning `_get_infosets` (rather than the public
- # History-returning `get_infosets`) because rendering needs each member's own
- # `.label`/`.number` for display, which a History alone cannot provide;
- # `Node.members` (still public) then gives the other members directly, and
- # `_history_of` recovers the History each one needs to index
- # `beliefs`/`realiz_probs` with.
- for infoset_number, node in enumerate(profile.game._get_infosets(player), start=1):
- for member in node.members:
- node_name = _name_or_number(member)
- member_history = _history_of(member)
- belief = beliefs[member_history]
- belief_text = format_value(belief, decimals) if belief is not None else ""
- realiz_text = format_value(realiz_probs[member_history], decimals)
- lines.append(
- f"{infoset_number:>7} {node_name:>7} "
- f"{belief_text:>11} {realiz_text:>11}"
- )
+ # One row per member of each of the player's information sets, identified by
+ # its History (see `_render_history`) rather than an engine-assigned node
+ # number: the `Infoset` column is still the same encounter-order index as the
+ # table above (a compact key for matching rows across the two tables, not a
+ # description -- the first member listed under each infoset number is always
+ # its canonical one, so its own History is already visible there for anyone
+ # who wants it).
+ rows = [
+ (infoset_number, _render_history(member.actions), beliefs[member],
+ realiz_probs[member])
+ for infoset_number, history in enumerate(profile.game.get_infosets(player), start=1)
+ for member in profile.game.get_members(gbt.H.path(*history.actions))
+ ]
+ history_width = max([len("History")] + [len(row[1]) for row in rows])
+ lines.append(f"Infoset {'History':<{history_width}} Belief Prob")
+ lines.append(f"------- {'-' * history_width} ----------- -----------")
+ for infoset_number, history_text, belief, realiz in rows:
+ belief_text = (
+ format_value(belief, decimals, fixed, as_float) if belief is not None else ""
+ )
+ lines.append(
+ f"{infoset_number:>7} {history_text:<{history_width}} "
+ f"{belief_text:>11} {format_value(realiz, decimals, fixed, as_float):>11}"
+ )
lines.append("")
+ # The probability distribution over plays: not player-specific (unlike the tables
+ # above), so printed once for the whole profile rather than once per player.
+ plays = [
+ (_render_history(play.actions), realiz_probs[play])
+ for play in profile.game.get_histories(gbt.H.plays)
+ ]
+ play_width = max([len("Play")] + [len(text) for text, _ in plays])
+ lines.append("Probability distribution over full histories:")
+ lines.append(f"{'Play':<{play_width}} Prob")
+ lines.append(f"{'-' * play_width} -----------")
+ for play_text, prob in plays:
+ prob_text = format_value(prob, decimals, fixed, as_float)
+ lines.append(f"{play_text:<{play_width}} {prob_text:>11}")
return "\n".join(lines)
diff --git a/src/pygambit/cli/enummixed.py b/src/pygambit/cli/enummixed.py
index 989d6b6e90..642ba80eae 100644
--- a/src/pygambit/cli/enummixed.py
+++ b/src/pygambit/cli/enummixed.py
@@ -31,6 +31,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
version_option,
)
@@ -55,27 +56,33 @@
help="compute using floating-point arithmetic; display results with DECIMALS digits",
)
@click.option("-c", "--cliques", is_flag=True, help="output connectedness information")
+@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, cliques: bool, quiet: bool) -> None:
+def main(
+ file: str | None, decimals: int | None, cliques: bool, detail: bool, quiet: bool
+) -> None:
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
rational = decimals is None
- def render(profile) -> None:
- click.echo(render_profile_csv(profile, "NE", decimals or 0))
+ def echo_profile(profile, label: str) -> None:
+ if detail:
+ click.echo(render_profile_detail(profile, decimals or 0))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals or 0))
result = gbt.nash.enummixed_solve(
game,
rational=rational,
- nash_callback=render,
+ nash_callback=lambda profile: echo_profile(profile, "NE"),
cliques=cliques,
)
if cliques:
for index, clique in enumerate(result.cliques, start=1):
label = f"convex-{index}"
for profile in clique:
- click.echo(render_profile_csv(profile, label, decimals or 0))
+ echo_profile(profile, label)
if __name__ == "__main__":
diff --git a/src/pygambit/cli/enumpoly.py b/src/pygambit/cli/enumpoly.py
index 9675c1d8ea..aef54b1d3c 100644
--- a/src/pygambit/cli/enumpoly.py
+++ b/src/pygambit/cli/enumpoly.py
@@ -40,6 +40,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
render_support_csv,
validate_stop_after,
version_option,
@@ -92,6 +93,7 @@
help="maximum number of rectangles to examine when searching for roots on a "
"single support, before giving up on that support",
)
+@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)")
@click.option(
"-V",
@@ -108,6 +110,7 @@ def main(
maxregret: float,
stop_after: int | None,
max_rectangles: int,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
@@ -116,7 +119,10 @@ def main(
raise ValueError("Computing equilibria of games with imperfect recall is not supported.")
def render(profile) -> None:
- click.echo(render_profile_csv(profile, "NE", decimals))
+ if detail:
+ click.echo(render_profile_detail(profile, decimals))
+ else:
+ click.echo(render_profile_csv(profile, "NE", decimals))
def render_event(event) -> None:
if not verbose:
diff --git a/src/pygambit/cli/gnm.py b/src/pygambit/cli/gnm.py
index 5ca1746ef6..a6d2a5fc08 100644
--- a/src/pygambit/cli/gnm.py
+++ b/src/pygambit/cli/gnm.py
@@ -36,6 +36,7 @@
print_banner,
read_game,
render_profile_csv,
+ render_profile_detail,
resolve_strategy_starts,
version_option,
)
@@ -115,6 +116,7 @@
type=int,
help="number of steps in each support cell",
)
+@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)")
@click.option(
"-V",
@@ -134,6 +136,7 @@ def main(
local_newton_interval: int,
local_newton_maxits: int,
steps: int,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
@@ -151,7 +154,10 @@ def main(
perturbations = resolve_strategy_starts(game, n_vectors, seed, start_file)
def render(profile, label: str = "NE") -> None:
- click.echo(render_profile_csv(profile, label, decimals))
+ if detail:
+ click.echo(render_profile_detail(profile, decimals))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals))
def render_event(event) -> None:
if not verbose:
diff --git a/src/pygambit/cli/hp.py b/src/pygambit/cli/hp.py
index add1c43656..202d9ac382 100644
--- a/src/pygambit/cli/hp.py
+++ b/src/pygambit/cli/hp.py
@@ -34,6 +34,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
resolve_strategy_starts,
version_option,
)
@@ -88,6 +89,7 @@
type=float,
help="maximum regret acceptable as a proportion of the range of payoffs in the 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)")
@click.option(
"-V",
@@ -104,23 +106,30 @@ def main(
seed: int | None,
start_file: str | None,
maxregret: float,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
priors = resolve_strategy_starts(game, n_priors, seed, start_file)
+ def render(profile, label: str) -> None:
+ if detail:
+ click.echo(render_profile_detail(profile, decimals))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals))
+
def render_event(event: gbt.HPStepEvent) -> None:
if verbose:
- click.echo(render_profile_csv(event.profile, f"{event.t:.6g}", decimals))
+ render(event.profile, f"{event.t:.6g}")
for prior in priors:
prior = prior.as_float()
if verbose:
- click.echo(render_profile_csv(prior, "prior", decimals))
+ render(prior, "prior")
result = gbt.nash.hp_solve(prior, maxregret=maxregret, event_callback=render_event)
if result.equilibrium is not None:
- click.echo(render_profile_csv(result.equilibrium, "NE", decimals))
+ render(result.equilibrium, "NE")
if __name__ == "__main__":
diff --git a/src/pygambit/cli/ipa.py b/src/pygambit/cli/ipa.py
index d12ed82352..6110a8029d 100644
--- a/src/pygambit/cli/ipa.py
+++ b/src/pygambit/cli/ipa.py
@@ -34,6 +34,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
resolve_strategy_starts,
version_option,
)
@@ -81,6 +82,7 @@
default=None,
help="file containing perturbation vectors (mutually exclusive with -n)",
)
+@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)")
@click.option(
"-V",
@@ -96,6 +98,7 @@ def main(
n_vectors: int | None,
seed: int | None,
start_file: str | None,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
@@ -103,7 +106,10 @@ def main(
perturbations = resolve_strategy_starts(game, n_vectors, seed, start_file)
def render(profile, label: str = "NE") -> None:
- click.echo(render_profile_csv(profile, label, decimals))
+ if detail:
+ click.echo(render_profile_detail(profile, decimals))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals))
def render_event(event) -> None:
if not verbose:
diff --git a/src/pygambit/cli/liap.py b/src/pygambit/cli/liap.py
index 4609de9702..23cefb6abd 100644
--- a/src/pygambit/cli/liap.py
+++ b/src/pygambit/cli/liap.py
@@ -34,6 +34,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
resolve_behavior_starts,
resolve_strategy_starts,
version_option,
@@ -102,6 +103,7 @@
default=None,
help="file containing starting points (mutually exclusive with -n)",
)
+@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)")
@click.option(
"-V",
@@ -120,6 +122,7 @@ def main(
maxiter: int,
maxregret: float,
start_file: str | None,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
@@ -127,7 +130,10 @@ def main(
use_agent = agent and game.is_tree
def render(profile, label: str = "NE") -> None:
- click.echo(render_profile_csv(profile, label, decimals))
+ if detail:
+ click.echo(render_profile_detail(profile, decimals))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals))
def render_event(event) -> None:
if not verbose:
diff --git a/src/pygambit/cli/logit.py b/src/pygambit/cli/logit.py
index 37f1bd5c65..791fbfd74a 100644
--- a/src/pygambit/cli/logit.py
+++ b/src/pygambit/cli/logit.py
@@ -33,6 +33,7 @@
handle_errors,
load_game,
render_profile_csv,
+ render_profile_detail,
version_option,
)
@@ -159,6 +160,16 @@ def _read_frequencies(path: str, game: gbt.Game) -> gbt.MixedStrategyProfileDoub
"bifurcations are already reported as part of the traced branch otherwise"
),
)
+@click.option(
+ "-D",
+ "--detail",
+ is_flag=True,
+ help=(
+ "print detailed information about the terminal equilibrium, if tracing reaches "
+ "one accepted as a Nash equilibrium (has no effect on points printed along the "
+ "way, or on -L/-l output, which are not themselves accepted equilibria)"
+ ),
+)
@click.option("-q", "--quiet", is_flag=True, help="quiet mode (suppresses banner)")
@version_option(DESCRIPTION)
@handle_errors
@@ -173,12 +184,19 @@ def main(
mle_file: str | None,
terminal_only: bool,
report_bifurcations: bool,
+ detail: bool,
quiet: bool,
) -> None:
game = load_game(quiet, DESCRIPTION, file, PROG_NAME)
if not game.is_perfect_recall:
raise ValueError("Computing equilibria of games with imperfect recall is not supported.")
+ def render_equilibrium(profile) -> None:
+ if detail:
+ click.echo(render_profile_detail(profile, decimals, fixed=False))
+ else:
+ click.echo(render_profile_csv(profile, "NE", decimals, fixed=False))
+
def stream(event) -> None:
if isinstance(event, gbt.LogitPathEvent):
click.echo(_render_qre_row(event.qre, decimals))
@@ -231,7 +249,8 @@ def stream_bifurcations_only(event) -> None:
max_accel=max_accel,
event_callback=event_callback,
)
- click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False))
+ if result.equilibrium is not None:
+ render_equilibrium(result.equilibrium)
return
result = gbt.nash.logit_solve(
@@ -242,7 +261,8 @@ def stream_bifurcations_only(event) -> None:
max_accel=max_accel,
event_callback=event_callback,
)
- click.echo(render_profile_csv(result.equilibrium, "NE", decimals, fixed=False))
+ if result.equilibrium is not None:
+ render_equilibrium(result.equilibrium)
if __name__ == "__main__":
diff --git a/src/pygambit/cli/simpdiv.py b/src/pygambit/cli/simpdiv.py
index 77939ec153..11881775a0 100644
--- a/src/pygambit/cli/simpdiv.py
+++ b/src/pygambit/cli/simpdiv.py
@@ -38,6 +38,7 @@
read_game,
read_strategy_profiles_csv,
render_profile_csv,
+ render_profile_detail,
version_option,
)
@@ -117,6 +118,7 @@ def _default_start(game: gbt.Game) -> gbt.MixedStrategyProfileRational:
help="maximum regret acceptable as a proportion of range of payoffs in the game "
"(default is 1/10000000)",
)
+@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)")
@click.option(
"-V",
@@ -135,6 +137,7 @@ def main(
start_file: str | None,
decimals: int,
maxregret: str | None,
+ detail: bool,
quiet: bool,
verbose: bool,
) -> None:
@@ -161,7 +164,10 @@ def main(
starts = [_default_start(game)]
def render(profile, label: str = "NE") -> None:
- click.echo(render_profile_csv(profile, label, decimals, as_float=as_float))
+ if detail:
+ click.echo(render_profile_detail(profile, decimals, as_float=as_float))
+ else:
+ click.echo(render_profile_csv(profile, label, decimals, as_float=as_float))
def render_event(event) -> None:
if not verbose:
diff --git a/tests/cli/test_enumpure.py b/tests/cli/test_enumpure.py
index 448111bf24..5e707abbee 100644
--- a/tests/cli/test_enumpure.py
+++ b/tests/cli/test_enumpure.py
@@ -27,8 +27,8 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_coordination_t
detail_result = cli_runner.invoke(enumpure.main, ["-q", "-D"], input=nfg_coordination_text)
assert detail_result.exit_code == 0
assert "NE," not in detail_result.stdout
- assert "Strategy profile for player 1:" in detail_result.stdout
- assert "Strategy profile for player 2:" in detail_result.stdout
+ assert "Strategy profile for 1:" in detail_result.stdout
+ assert "Strategy profile for 2:" in detail_result.stdout
assert len(detail_result.stdout.splitlines()) > len(csv_result.stdout.splitlines())
diff --git a/tests/cli/test_lcp.py b/tests/cli/test_lcp.py
index fa1a578cb8..ae279963a7 100644
--- a/tests/cli/test_lcp.py
+++ b/tests/cli/test_lcp.py
@@ -19,7 +19,7 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_coordination_t
result = cli_runner.invoke(lcp.main, ["-q", "-D"], input=nfg_coordination_text)
assert result.exit_code == 0
assert "NE," not in result.stdout
- assert "Strategy profile for player 1:" in result.stdout
+ assert "Strategy profile for 1:" in result.stdout
def test_strategic_flag_switches_tree_game_to_strategy_representation(
diff --git a/tests/cli/test_lp.py b/tests/cli/test_lp.py
index 1d3767e101..3292644cbe 100644
--- a/tests/cli/test_lp.py
+++ b/tests/cli/test_lp.py
@@ -19,7 +19,7 @@ def test_detail_flag_switches_to_multiline_report(cli_runner, nfg_matching_penni
result = cli_runner.invoke(lp.main, ["-q", "-D"], input=nfg_matching_pennies_text)
assert result.exit_code == 0
assert "NE," not in result.stdout
- assert "Strategy profile for player 1:" in result.stdout
+ assert "Strategy profile for 1:" in result.stdout
def test_strategic_flag_switches_tree_game_to_strategy_representation(
@@ -33,8 +33,8 @@ def test_strategic_flag_switches_tree_game_to_strategy_representation(
strategic_result = cli_runner.invoke(lp.main, ["-q", "-D", "-S"], input=efg_small_tree_text)
assert behavior_result.exit_code == 0
assert strategic_result.exit_code == 0
- assert "Behavior profile for player 1:" in behavior_result.stdout
- assert "Strategy profile for player 1:" in strategic_result.stdout
+ assert "Behavior profile for 1:" in behavior_result.stdout
+ assert "Strategy profile for 1:" in strategic_result.stdout
def test_non_constant_sum_game_is_a_clean_error(cli_runner, nfg_coordination_text):