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
3 changes: 3 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
- Added `Game.relabel_players`, which simultaneously reassigns the labels of the game's players. (#1058)
- Added `max_rectangles` to `enumpoly_solve` (and `-r` to `gambit-enumpoly`), bounding the number
of cells examined when searching for equilibria on a single support. (#1055)
- Added `Game.set_players`, which declares the ordered list of the game's players, matching by label. (#1059)

### Fixed
- `MixedStrategy.__eq__` raised `AttributeError` when comparing two `MixedStrategy` instances
Expand Down Expand Up @@ -185,6 +186,8 @@
equivalent `SetStrategies` call directly. (#1056)
- Assigning to `Player.label` has been removed; use `Game.relabel_players`, which enforces
nonempty, unique labels. (#1058)
- `Game.add_player` has been removed; use `Game.set_players`, specifying the labels
of the players. (#1059)


## [17.0.0-alpha.1] - 2026-08-13
Expand Down
1 change: 0 additions & 1 deletion Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ EXTRA_DIST = \
src/gui/bitmaps/layout.xpm \
src/gui/bitmaps/move.xpm \
src/gui/bitmaps/newcol.xpm \
src/gui/bitmaps/newplayer.xpm \
src/gui/bitmaps/newrow.xpm \
src/gui/bitmaps/newtable.xpm \
src/gui/bitmaps/newtree.xpm \
Expand Down
18 changes: 11 additions & 7 deletions doc/gui.general.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ The frame presenting a game consists of a single main panel, which displays
the game graphically; in this case, showing the game tree of a simple
one-card poker game. Note that where applicable, information is color-coded
to match the colors assigned to the players: Fred's moves and payoffs are
presented in red, and Alice's in blue. Player names and colors are set on
the :guilabel:`Players` page of the :guilabel:`Game properties` dialog
(:menuselection:`Edit --> Game`): each player is listed with a text field
for its name and a color swatch beside it, which opens a color picker when
clicked. A new player is added to the game using
:menuselection:`Edit --> Add player`; the Players page itself does not yet
support adding, removing, or reordering players.
presented in red, and Alice's in blue. Players are added, removed, reordered,
renamed, and recolored on the :guilabel:`Players` page of the
:guilabel:`Game properties` dialog (:menuselection:`Edit --> Game`), in the
same way as a player's strategies are edited on the :guilabel:`Edit
strategies` dialog (see :ref:`Adding, removing, and reordering strategies
<editing-strategies>`): each player has a text field for its name, a color
swatch beside it that opens a color picker when clicked, and
:guilabel:`↑`/:guilabel:`↓`/:guilabel:`✕` buttons to reorder or remove it.
A player can't be removed if it has decisions in the game, or more than one
strategy in the game's strategic representation; its :guilabel:`✕` button is
disabled, with a tooltip explaining why.

Hovering the mouse pointer over a node in the tree briefly displays a small
window showing, for each player, that player's expected payoff from that
Expand Down
10 changes: 6 additions & 4 deletions doc/gui.nfg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,10 @@ continues to identify the player to whom it belongs.
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`.
Players are added, removed, and reordered on the :guilabel:`Players` page of
the :guilabel:`Game properties` dialog (:menuselection:`Edit --> Game`); see
:doc:`gui.general`. A newly added player has one strategy, by default
labeled with the number :guilabel:`1`.


Editing strategies
Expand All @@ -157,6 +157,8 @@ 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.

.. _editing-strategies:

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

Expand Down
2 changes: 1 addition & 1 deletion doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ Transforming game components
.. autosummary::
:toctree: api/

Game.add_player
Game.relabel_players
Game.set_players
Game.add_outcome
Game.delete_outcome
Game.set_outcome
Expand Down
4 changes: 2 additions & 2 deletions src/games/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -589,8 +589,8 @@ void ReadPlayers(GameFileLexer &p_state, Game &p_game, TreeData &p_treeData)
}
p_state.ExpectCurrentToken(TOKEN_RBRACE, "'}'");
NormalizeLabelStrings(player_labels);
for (const auto &label : player_labels) {
p_game->NewPlayer(label);
if (!player_labels.empty()) {
p_game->SetPlayers(player_labels);
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -1356,10 +1356,10 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
/// Returns the chance (nature) player
virtual GamePlayer GetChance() const = 0;
auto GetPlayersWithChance() const { return prepend_value(GetChance(), GetPlayers()); }
/// Creates a new player in the game, with no moves
virtual GamePlayer NewPlayer(const std::string &p_label) = 0;
/// Reassign player labels. Keys of p_labels are current labels; values are their replacements.
void RelabelPlayers(const std::map<std::string, std::string> &p_labels);
/// Declare the ordered list of players of the game.
virtual void SetPlayers(const std::vector<std::string> &) { throw UndefinedException(); }
//@}

/// @name Dimensions of the game
Expand Down Expand Up @@ -1707,8 +1707,8 @@ inline Game GameSubgameRep::GetGame() const { return m_game->shared_from_this();

//=======================================================================

/// Factory function to create new game tree
[[nodiscard]] Game NewTree();
/// Factory function to create new game tree,
[[nodiscard]] Game NewTree(const std::vector<std::string> &p_players = {});
/// Factory function to create new game table
[[nodiscard]] Game NewTable(const std::vector<int> &p_dim, bool p_sparseOutcomes = false);

Expand Down
2 changes: 0 additions & 2 deletions src/games/gameagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ class GameAGGRep : public GameRep {
//@{
/// Returns the chance (nature) player
GamePlayer GetChance() const override { throw UndefinedException(); }
/// Creates a new player in the game, with no moves
GamePlayer NewPlayer(const std::string &) override { throw UndefinedException(); }
//@}

/// @name Nodes
Expand Down
2 changes: 0 additions & 2 deletions src/games/gamebagg.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,6 @@ class GameBAGGRep : public GameRep {
//@{
/// Returns the chance (nature) player
GamePlayer GetChance() const override { throw UndefinedException(); }
/// Creates a new player in the game, with no moves
GamePlayer NewPlayer(const std::string &) override { throw UndefinedException(); }
//@}

/// @name Nodes
Expand Down
108 changes: 94 additions & 14 deletions src/games/gametable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -493,20 +493,6 @@ void GameTableRep::WriteNfgFile(std::ostream &p_file) const
// GameTableRep: Players
//------------------------------------------------------------------------

GamePlayer GameTableRep::NewPlayer(const std::string &p_label)
{
CheckPlayerLabel(p_label);
auto player = std::make_shared<GamePlayerRep>(this, m_players.size() + 1, p_label, 1);
player->m_strategies.front()->m_label = "1";
IncrementVersion();
m_players.push_back(player);
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs[player.get()] = Number();
}
IndexStrategies();
return player;
}

//------------------------------------------------------------------------
// GameTableRep: Outcomes
//------------------------------------------------------------------------
Expand Down Expand Up @@ -630,6 +616,100 @@ void GameTableRep::SetStrategies(const GamePlayer &p_player,
RebuildTable(old_radices, p_player->GetNumber() - 1, old_to_new);
}

void GameTableRep::SetPlayers(const std::vector<std::string> &p_labels)
{
if (p_labels.empty()) {
throw ValueException("At least one player must be specified");
}
std::map<std::string, long> current;
for (const auto &player : m_players) {
if (!current.emplace(player->GetLabel(), player->GetNumber() - 1).second) {
throw ValueException("Player label '" + player->GetLabel() + "' is ambiguous in this game");
}
}
std::set<std::string> declared;
for (const auto &label : p_labels) {
if (!declared.insert(label).second) {
throw ValueException("Player label '" + label + "' appears more than once");
}
if (current.count(label) == 0) {
CheckPlayerLabel(label);
}
}
std::vector<long> old_radices;
old_radices.reserve(m_players.size());
for (const auto &player : m_players) {
if (declared.count(player->GetLabel()) == 0 && player->m_strategies.size() != 1) {
throw UndefinedException("A player with more than one strategy cannot be deleted");
}
old_radices.push_back(player->m_strategies.size());
}
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);
}

IncrementVersion();
std::vector<std::shared_ptr<GamePlayerRep>> newPlayers;
newPlayers.reserve(p_labels.size());
for (size_t j = 0; j < p_labels.size(); ++j) {
if (source[j] >= 0) {
newPlayers.push_back(m_players[source[j]]);
continue;
}
auto player = std::make_shared<GamePlayerRep>(this, static_cast<int>(j) + 1, p_labels[j], 1);
player->m_strategies.front()->m_label = "1";
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs[player.get()] = Number();
}
newPlayers.push_back(player);
}
for (const auto &player : m_players) {
if (declared.count(player->GetLabel()) == 0) {
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs.erase(player.get());
}
player->Invalidate();
}
}
m_players = std::move(newPlayers);
for (size_t j = 0; j < m_players.size(); ++j) {
m_players[j]->m_number = static_cast<int>(j) + 1;
}
// Permute the outcome table into the new player order.
std::vector<long> old_strides(old_radices.size());
long stride = 1;
for (size_t i = 0; i < old_radices.size(); ++i) {
old_strides[i] = stride;
stride *= old_radices[i];
}
const long old_size = stride;
std::vector<long> new_strides(m_players.size());
long new_size = 1;
for (size_t j = 0; j < m_players.size(); ++j) {
new_strides[j] = new_size;
new_size *= m_players[j]->m_strategies.size();
}
std::vector<GameOutcomeRep *> newResults(new_size, nullptr);
for (long old_index = 0; old_index < old_size; ++old_index) {
if (m_results[old_index] == nullptr) {
continue;
}
long new_index = 0;
for (size_t j = 0; j < m_players.size(); ++j) {
if (source[j] >= 0) {
new_index +=
((old_index / old_strides[source[j]]) % old_radices[source[j]]) * new_strides[j];
}
}
newResults[new_index] = m_results[old_index];
}
m_results.swap(newResults);
IndexStrategies();
}

//------------------------------------------------------------------------
// GameTableRep: Factory functions
//------------------------------------------------------------------------
Expand Down
3 changes: 1 addition & 2 deletions src/games/gametable.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@ class GameTableRep : public GameExplicitRep {
//@{
/// Returns the chance (nature) player
GamePlayer GetChance() const override { throw UndefinedException(); }
/// Creates a new player in the game, with no moves
GamePlayer NewPlayer(const std::string &p_label) override;
void SetPlayers(const std::vector<std::string> &) override;
//@}

/// @name Nodes
Expand Down
71 changes: 63 additions & 8 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -927,7 +927,14 @@ Game GameTreeRep::Copy() const
return ReadGame(is);
}

Game NewTree() { return std::make_shared<GameTreeRep>(); }
Game NewTree(const std::vector<std::string> &p_players)
{
auto game = std::make_shared<GameTreeRep>();
if (!p_players.empty()) {
game->SetPlayers(p_players);
}
return game;
}

//------------------------------------------------------------------------
// GameTreeRep: General data access
Expand Down Expand Up @@ -1706,17 +1713,65 @@ int GameTreeRep::BehavProfileLength() const
// GameTreeRep: Players
//------------------------------------------------------------------------

GamePlayer GameTreeRep::NewPlayer(const std::string &p_label)
void GameTreeRep::SetPlayers(const std::vector<std::string> &p_labels)
{
CheckPlayerLabel(p_label);
auto player = std::make_shared<GamePlayerRep>(this, m_players.size() + 1, p_label);
if (p_labels.empty()) {
throw ValueException("At least one player must be specified");
}
std::map<std::string, long> current;
for (const auto &player : m_players) {
if (!current.emplace(player->GetLabel(), player->GetNumber() - 1).second) {
throw ValueException("Player label '" + player->GetLabel() + "' is ambiguous in this game");
}
}
std::set<std::string> declared;
for (const auto &label : p_labels) {
if (!declared.insert(label).second) {
throw ValueException("Player label '" + label + "' appears more than once");
}
if (current.count(label) == 0) {
CheckPlayerLabel(label);
}
}
for (const auto &player : m_players) {
if (declared.count(player->GetLabel()) == 0 && !player->m_infosets.empty()) {
throw UndefinedException("A player who has decisions in the game cannot be deleted");
}
}
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);
}

IncrementVersion();
m_players.push_back(player);
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs[player.get()] = Number();
std::vector<std::shared_ptr<GamePlayerRep>> newPlayers;
newPlayers.reserve(p_labels.size());
for (size_t j = 0; j < p_labels.size(); ++j) {
if (source[j] >= 0) {
newPlayers.push_back(m_players[source[j]]);
continue;
}
auto player = std::make_shared<GamePlayerRep>(this, static_cast<int>(j) + 1, p_labels[j]);
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs[player.get()] = Number();
}
newPlayers.push_back(player);
}
for (const auto &player : m_players) {
if (declared.count(player->GetLabel()) == 0) {
for (const auto &outcome : m_outcomes) {
outcome->m_payoffs.erase(player.get());
}
player->Invalidate();
}
}
m_players = std::move(newPlayers);
for (size_t j = 0; j < m_players.size(); ++j) {
m_players[j]->m_number = static_cast<int>(j) + 1;
}
ClearComputedValues();
return player;
}

//------------------------------------------------------------------------
Expand Down
3 changes: 1 addition & 2 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,7 @@ class GameTreeRep final : public GameExplicitRep {
//@{
/// Returns the chance (nature) player
GamePlayer GetChance() const override { return m_chance->shared_from_this(); }
/// Creates a new player in the game, with no moves
GamePlayer NewPlayer(const std::string &p_label) override;
void SetPlayers(const std::vector<std::string> &) override;
//@}

/// @name Nodes
Expand Down
Loading
Loading