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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

### Added
- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047)
- Added `Game.set_strategies`, which declares the ordered list of a player's strategies in a game in strategic form,
matching by label. (#1056)
- `gambit-ipa` now supports verbose output via the `-V` flag, reporting on intermediate progress. (#501)
- Added `-R` option to `gambit-liap`, `gambit-simpdiv`, `gambit-ipa`, and `gambit-gnm` to reproducibly seed
random starting point generation. (#448)
Expand All @@ -20,10 +22,20 @@
command-line executables as external processes. (#990)
- When displaying a strategic form, the graphical interface preserves decimal payoffs when those are
what are specified in the game, whenever possible. (#867)
- The GUI's strategic game table gained an "Edit strategies" dialog, opened by clicking any of a
player's strategy labels, supporting adding, deleting, reordering, and renaming that player's
strategies in one operation. The "+" toolbar icon for adding a strategy, and the strategy
label's right-click "Delete strategy" menu, are removed, superseded by the dialog. (#1056)

### Removed
- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces
nonempty, unique labels. (#1047)
- `Game.add_strategy` and `Game.delete_strategy` have been removed; use `Game.set_strategies`,
specifying the labels of the strategies the player is to have. (#1056)
- The C++ `Game` interface's `NewStrategy` and `DeleteStrategy` methods have been removed; they
were subsumed by `SetStrategies`, and by this point were used only in support of `pygambit`'s
`add_strategy`/`delete_strategy` and the GUI's strategy add/delete controls, which now build the
equivalent `SetStrategies` call directly. (#1056)


## [17.0.0-alpha.1] - 2026-08-13
Expand Down
4 changes: 2 additions & 2 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -478,14 +478,14 @@ gambit_SOURCES = \
src/gui/editlabel.h \
src/gui/edittext.cc \
src/gui/edittext.h \
src/gui/labelcell.cc \
src/gui/labelcell.h \
src/gui/dlabout.cc \
src/gui/dlabout.h \
src/gui/dleditmove.cc \
src/gui/dleditmove.h \
src/gui/dleditnode.cc \
src/gui/dleditnode.h \
src/gui/dleditstrategies.cc \
src/gui/dleditstrategies.h \
src/gui/dlefglayout.cc \
src/gui/dlefglayout.h \
src/gui/dlefglegend.cc \
Expand Down
86 changes: 72 additions & 14 deletions doc/gui.nfg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,27 +126,85 @@ continues to identify the player to whom it belongs.
does not change the players, strategies, outcomes, or payoffs.


Changing players and strategies
-------------------------------
Adding players
--------------

To add an additional player to the game, use the menu item
:menuselection:`Edit --> Add player`,
or the corresponding toolbar icon . The newly created player
has one strategy, by default labeled with the number :guilabel:`1`.

Gambit supports arbitrary numbers of strategies for each player. To
add a new strategy for a player, click the new strategy icon located
to the left of that player's name.

To edit the names of strategies, click on any cell in the strategic
game table where the strategy label appears, and edit the label using
the edit control.

Right-clicking a strategy label creates a popup context menu.
This menu offers the ability to delete the selected strategy
from the game. It is not possible to delete a player's only strategy.


Editing strategies
-------------------

A player's strategies can be added, removed, reordered, and renamed by
clicking any cell in the strategic game table where one of that
player's strategy labels appears. This opens an :guilabel:`Edit
strategies` dialog for that player, titled with the player's own
label, listing a row for each of the player's strategies and showing
its label.

Adding, removing, and reordering strategies
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Clicking :guilabel:`+`, below the list of strategies, adds a new
strategy with an automatically generated numeric label, which can be
relabeled like any other strategy. Each strategy's row has a
:guilabel:`✕` button to remove it, and :guilabel:`↑`/:guilabel:`↓`
buttons to move it earlier or later among the player's other
strategies.

Removing a strategy does not immediately discard the payoffs at the
contingencies that use it. Instead, the row is shown disabled, with
its label struck through, and its :guilabel:`✕` button is replaced by
a :guilabel:`↺` (restore) button, so that what is about to be
destroyed remains visible -- and reversible -- until the dialog is
confirmed. Clicking :guilabel:`↺` returns the strategy, and the
payoffs at every contingency involving it, exactly as they were.
Nothing is actually deleted until :guilabel:`OK` is clicked; clicking
:guilabel:`Cancel` leaves the player's strategies entirely unchanged.
At least one strategy must remain: the last remaining strategy cannot
be removed.

Renaming versus reordering
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Typing a new label into a strategy's row, and moving that row to a
different position with the :guilabel:`↑`/:guilabel:`↓` buttons, are
two distinct operations. They can be combined, but neither substitutes
for the other:

+ Changing the text in a row renames that strategy, wherever it ends
up in the list. The payoffs at every contingency involving it travel
with it: renaming a strategy never changes any payoff.
+ Moving a row up or down changes the strategy's position among the
player's other strategies, permuting the payoff table to match. Each
strategy keeps its own payoffs as it moves: reordering a strategy
never changes what it is called.

Because these are separate operations, two strategies can have their
labels swapped in place -- for instance, relabeling :guilabel:`Cooperate`
to :guilabel:`Defect` and :guilabel:`Defect` to :guilabel:`Cooperate` at
the same time, since all the relabeling for a player happens
simultaneously when :guilabel:`OK` is clicked -- without touching the
payoffs at any contingency. This is different from using the
:guilabel:`↑`/:guilabel:`↓` buttons to swap the *positions* of
:guilabel:`Cooperate` and :guilabel:`Defect`, which exchanges the
payoffs at the contingencies where each is chosen, while leaving each
strategy's own label attached to it.

To help keep track of which is which while editing, a strategy whose
label has been changed from what it started as is shown in italic blue
text, and shows the original label in a tooltip on hover; a newly
added strategy is shown in bold green text.

Strategy labels must be nonempty, and unique among the strategies
currently kept for the player (a removed strategy's label is not
considered). Any field that currently violates one of these rules is
highlighted, and a description of the problem is shown below the list
of strategies; the :guilabel:`OK` button is disabled until all fields
are valid.


Editing payoffs
Expand Down
3 changes: 1 addition & 2 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,8 @@ Transforming game components
Game.add_outcome
Game.delete_outcome
Game.set_outcome
Game.add_strategy
Game.delete_strategy
Game.relabel_strategies
Game.set_strategies


Information about the game
Expand Down
12 changes: 5 additions & 7 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -1378,19 +1378,17 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
}
return *std::next(strategies.begin(), p_index - 1);
}
/// Creates a new strategy for the player
virtual GameStrategy NewStrategy(const GamePlayer &p_player, const std::string &p_label)
{
throw UndefinedException();
}
/// Remove the strategy from the game
virtual void DeleteStrategy(const GameStrategy &p_strategy) { throw UndefinedException(); }
/// Simultaneously reassign strategy labels for a player: keys are current labels.
/// Keys of p_labels are current action labels; values are their replacements.
virtual void RelabelStrategies(const GamePlayer &, const std::map<std::string, std::string> &)
{
throw UndefinedException();
}
/// Declare the ordered list of strategies of a player.
virtual void SetStrategies(const GamePlayer &, const std::vector<std::string> &)
{
throw UndefinedException();
}
/// Returns the total number of actions in the game
virtual int BehavProfileLength() const = 0;
//@}
Expand Down
133 changes: 80 additions & 53 deletions src/games/gametable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ GamePlayer GameTableRep::NewPlayer(const std::string &p_label)
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs[player.get()] = Number();
}
IndexStrategies();
return player;
}

Expand All @@ -527,47 +528,6 @@ void GameTableRep::DeleteOutcome(const GameOutcome &p_outcome)
// GameTableRep: Strategies
//------------------------------------------------------------------------

GameStrategy GameTableRep::NewStrategy(const GamePlayer &p_player, const std::string &p_label)
{
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
p_player->CheckStrategyLabel(p_label, {});
auto strategy = std::make_shared<GameStrategyRep>(p_player.get(),
p_player->m_strategies.size() + 1, p_label);
IncrementVersion();
std::vector<long> old_radices;
for (const auto &player : m_players) {
old_radices.push_back(player->m_strategies.size());
}
p_player->m_strategies.push_back(strategy);
RebuildTable(old_radices);
return p_player->m_strategies.back();
}

void GameTableRep::DeleteStrategy(const GameStrategy &p_strategy)
{
const auto player = p_strategy->GetPlayer().get();
if (player->m_game != this) {
throw MismatchException();
}
if (player->GetStrategies().size() == 1) {
return;
}

IncrementVersion();
std::vector<long> old_radices;
for (const auto &pl : m_players) {
old_radices.push_back(pl->m_strategies.size());
}
const auto deletedIt = std::find(player->m_strategies.begin(), player->m_strategies.end(),
std::shared_ptr<GameStrategyRep>(p_strategy));
const long deletedDigit = deletedIt - player->m_strategies.begin();
player->m_strategies.erase(deletedIt);
RebuildTable(old_radices, player->GetNumber() - 1, deletedDigit);
p_strategy->Invalidate();
}

void GameTableRep::RelabelStrategies(const GamePlayer &p_player,
const std::map<std::string, std::string> &p_labels)
{
Expand Down Expand Up @@ -605,6 +565,71 @@ void GameTableRep::RelabelStrategies(const GamePlayer &p_player,
}
}

void GameTableRep::SetStrategies(const GamePlayer &p_player,
const std::vector<std::string> &p_labels)
{
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
if (p_labels.empty()) {
throw ValueException("At least one strategy must be specified");
}
std::set<std::string> declared;
for (const auto &label : p_labels) {
if (label.empty()) {
throw ValueException("Strategy label must not be empty");
}
CheckLabel(label);
if (!declared.insert(label).second) {
throw ValueException("Strategy label '" + label + "' appears more than once");
}
}
// Match declared labels against current strategies.
std::map<std::string, long> current;
for (const auto &strategy : p_player->m_strategies) {
if (!current.emplace(strategy->GetLabel(), strategy->GetNumber() - 1).second) {
throw ValueException("Strategy label '" + strategy->GetLabel() +
"' is ambiguous for this player");
}
}
std::vector<long> source;
source.reserve(p_labels.size());
for (const auto &label : p_labels) {
const auto it = current.find(label);
source.push_back((it != current.end()) ? it->second : -1);
}
std::vector<long> old_to_new(p_player->m_strategies.size(), -1);
for (size_t i = 0; i < source.size(); ++i) {
if (source[i] >= 0) {
old_to_new[source[i]] = static_cast<long>(i);
}
}
std::vector<long> old_radices;
for (const auto &player : m_players) {
old_radices.push_back(player->m_strategies.size());
}

IncrementVersion();
std::vector<std::shared_ptr<GameStrategyRep>> newStrategies;
newStrategies.reserve(p_labels.size());
for (size_t i = 0; i < p_labels.size(); ++i) {
if (source[i] >= 0) {
newStrategies.push_back(p_player->m_strategies[source[i]]);
}
else {
newStrategies.push_back(
std::make_shared<GameStrategyRep>(p_player.get(), static_cast<int>(i) + 1, p_labels[i]));
}
}
for (const auto &strategy : p_player->m_strategies) {
if (declared.count(strategy->GetLabel()) == 0) {
strategy->Invalidate();
}
}
p_player->m_strategies = std::move(newStrategies);
RebuildTable(old_radices, p_player->GetNumber() - 1, old_to_new);
}

//------------------------------------------------------------------------
// GameTableRep: Factory functions
//------------------------------------------------------------------------
Expand Down Expand Up @@ -639,9 +664,9 @@ GameTableRep::NewMixedStrategyProfile(const Rational &, const StrategySupportPro

/// This rebuilds a new table of outcomes after the game has been
/// redimensioned (change in the number of strategies). See the declaration
/// in gametable.h for the meaning of p_deletedPlayer/p_deletedDigit.
void GameTableRep::RebuildTable(const std::vector<long> &old_radices, long p_deletedPlayer,
long p_deletedDigit)
/// in gametable.h for the meaning of p_player/p_oldToNew.
void GameTableRep::RebuildTable(const std::vector<long> &old_radices, long p_player,
const std::vector<long> &p_oldToNew)
{
std::vector<long> old_strides(old_radices.size());
long stride = 1;
Expand All @@ -663,21 +688,23 @@ void GameTableRep::RebuildTable(const std::vector<long> &old_radices, long p_del
if (m_results[old_index] == nullptr) {
continue;
}
if (p_deletedPlayer >= 0 &&
(old_index / old_strides[p_deletedPlayer]) % old_radices[p_deletedPlayer] ==
p_deletedDigit) {
// This contingency used the strategy that was deleted.
continue;
}
long new_index = 0;
bool dropped = false;
for (size_t i = 0; i < m_players.size(); ++i) {
long digit = (old_index / old_strides[i]) % old_radices[i];
if (static_cast<long>(i) == p_deletedPlayer && digit > p_deletedDigit) {
--digit;
if (static_cast<long>(i) == p_player) {
digit = p_oldToNew[digit];
if (digit < 0) {
// This contingency used a strategy that was removed.
dropped = true;
break;
}
}
new_index += digit * new_strides[i];
}
newResults[new_index] = m_results[old_index];
if (!dropped) {
newResults[new_index] = m_results[old_index];
}
}
m_results.swap(newResults);
IndexStrategies();
Expand Down
9 changes: 5 additions & 4 deletions src/games/gametable.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ class GameTableRep : public GameExplicitRep {

/// @name Private auxiliary functions
//@{
void RebuildTable(const std::vector<long> &old_radices, long p_deletedPlayer = -1,
long p_deletedDigit = -1);
/// Rebuild the outcome table after the strategies of p_player have changed.
/// p_oldToNew maps old strategy indices to new index, or to -1 if the strategy was removed.
void RebuildTable(const std::vector<long> &old_radices, long p_player,
const std::vector<long> &p_oldToNew);
//@}

public:
Expand Down Expand Up @@ -98,9 +100,8 @@ class GameTableRep : public GameExplicitRep {

/// @name Strategies
//@{
GameStrategy NewStrategy(const GamePlayer &, const std::string &) override;
void DeleteStrategy(const GameStrategy &p_strategy) override;
void RelabelStrategies(const GamePlayer &, const std::map<std::string, std::string> &) override;
void SetStrategies(const GamePlayer &, const std::vector<std::string> &) override;
//@}

/// @name Writing data files
Expand Down
Loading
Loading