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
6 changes: 6 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
its actions to be specified up front, just as `Game.make_event` does. (#999)
- `Game.get_behavior` returns a map-like view of the actions a reduced strategy prescribes at its player's
information sets; the unreachable information sets are absent. (#1002)
- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047)

### Changed
- Adopted the C++20 standard; build system now enforces exactly C++20 (not a minimum) (cf. #1027)
Expand Down Expand Up @@ -59,6 +60,9 @@
### Fixed
- Converting a behavior profile to a strategy profile, and computing pure-strategy payoffs
no longer inserts spurious entries into a strategy's internal action map.
- `Game.add_player`, on a game in strategic form, no longer leaves the new player's sole
strategy with an empty label; it is now labeled `"1"`, as `Game.new_table` already labels
every other player's initial strategies. (#1047)

### Removed
- `Game.set_player` has been removed; use `Game.make_infoset`, specifying the members of the
Expand All @@ -83,6 +87,8 @@
stub overrides it forced on the strategic, AGG, and BAGG representations; it was subsumed by
`MakeEvent`, and by this point was used only in support of the GUI's "Edit Move" dialog, which
now builds the equivalent `MakeEvent` call directly. (#999)
- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces
nonempty, unique labels. (#1047)

## [16.7.0] - 2026-07-11

Expand Down
1 change: 1 addition & 0 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Transforming game components
Game.set_outcome
Game.add_strategy
Game.delete_strategy
Game.relabel_strategies


Information about the game
Expand Down
11 changes: 1 addition & 10 deletions doc/tutorials/01_quickstart.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,7 @@
"id": "9d8203e8",
"metadata": {},
"outputs": [],
"source": [
"tom, jerry = g.players\n",
"tom.label = \"Tom\"\n",
"jerry.label = \"Jerry\"\n",
"\n",
"for player in g.players:\n",
" cooperate, defect = player.strategies\n",
" cooperate.label = \"Cooperate\"\n",
" defect.label = \"Defect\""
]
"source": "tom, jerry = g.players\ntom.label = \"Tom\"\njerry.label = \"Jerry\"\n\nfor player in g.players:\n cooperate, defect = player.strategies\n g.relabel_strategies(player, {cooperate.label: \"Cooperate\", defect.label: \"Defect\"})"
},
{
"cell_type": "markdown",
Expand Down
38 changes: 3 additions & 35 deletions doc/tutorials/interoperability_tutorials/gamut.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -387,43 +387,11 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "gamut-bos-gen",
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"<center><h1>Chicken</h1></center>\n",
"<table><tr><td colspan=\"2\" rowspan=\"2\"></td><td colspan=\"2\" align=\"center\"><b>Player2</b></td></tr><tr><td align=\"center\"><b>Swerve</b></td><td align=\"center\"><b>Straight</b></td></tr><tr><td rowspan=\"2\" align=\"center\" valign=\"middle\"><b>Player1</b></td><td align=\"center\"><b>Swerve</b></td><td align=center>2,2</td><td align=center>1,4</td></tr><tr><td align=\"center\"><b>Straight</b></td><td align=center>4,1</td><td align=center>0,0</td></tr></table>\n"
],
"text/plain": [
"Game(title='Chicken')"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"g_chicken = gbt.catalog.generate_gamut(\n",
" \"Chicken\",\n",
" params={\n",
" \"int_payoffs\": True,\n",
" \"int_mult\": 1,\n",
" \"normalize\": True,\n",
" \"min_payoff\": 0,\n",
" \"max_payoff\": 4,\n",
" },\n",
" gamut_jar=\"~/Downloads/gamut.jar\",\n",
")\n",
"g_chicken.title = \"Chicken\"\n",
"for player in g_chicken.players:\n",
" for strategy, label in zip(player.strategies, [\"Swerve\", \"Straight\"], strict=True):\n",
" strategy.label = label\n",
"g_chicken"
]
"outputs": [],
"source": "g_chicken = gbt.catalog.generate_gamut(\n \"Chicken\",\n params={\n \"int_payoffs\": True,\n \"int_mult\": 1,\n \"normalize\": True,\n \"min_payoff\": 0,\n \"max_payoff\": 4,\n },\n gamut_jar=\"~/Downloads/gamut.jar\",\n)\ng_chicken.title = \"Chicken\"\nfor player in g_chicken.players:\n labels = {strategy.label: label\n for strategy, label in zip(player.strategies, [\"Swerve\", \"Straight\"], strict=True)}\n g_chicken.relabel_strategies(player, labels)\ng_chicken"
},
{
"cell_type": "markdown",
Expand Down
12 changes: 1 addition & 11 deletions doc/tutorials/interoperability_tutorials/openspiel.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -156,17 +156,7 @@
"id": "b684325e",
"metadata": {},
"outputs": [],
"source": [
"gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n",
"\n",
"gbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n",
"\n",
"for player in gbt_matrix_rps_game.players:\n",
" for strategy, name in zip(player.strategies, [\"Rock\", \"Paper\", \"Scissors\"], strict=True):\n",
" strategy.label = name\n",
"\n",
"gbt_matrix_rps_game"
]
"source": "gbt_matrix_rps_game = gbt.catalog.generate_openspiel(\"matrix_rps\")\n\ngbt_matrix_rps_game.title = \"Rock-Paper-Scissors\"\n\nfor player in gbt_matrix_rps_game.players:\n names = [\"Rock\", \"Paper\", \"Scissors\"]\n labels = {strategy.label: name\n for strategy, name in zip(player.strategies, names, strict=True)}\n gbt_matrix_rps_game.relabel_strategies(player, labels)\n\ngbt_matrix_rps_game"
},
{
"cell_type": "markdown",
Expand Down
16 changes: 10 additions & 6 deletions src/games/file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,12 @@ Game BuildNfg(GameFileLexer &p_parser, TableFileGame &p_data)
strategyLabels.push_back(p_data.GetStrategy(player->GetNumber(), strategy->GetNumber()));
}
NormalizeLabelStrings(strategyLabels);
RelabelWithoutCollision(player->GetStrategies(), strategyLabels);
std::map<std::string, std::string> labels;
size_t index = 0;
for (auto strategy : player->GetStrategies()) {
labels[strategy->GetLabel()] = strategyLabels[index++];
}
nfg->RelabelStrategies(player, labels);
}

if (p_parser.GetCurrentToken() == TOKEN_LBRACE) {
Expand Down Expand Up @@ -907,11 +912,10 @@ void NormalizeGameLabels(const Game &p_game)
// Action labels are not normalized here: for tree games, ParseNode/ParsePersonalNode
// already normalize each infoset's actions individually, at creation, from the raw
// labels as parsed (see there for why the raw labels must be kept around too).
if (!p_game->IsTree()) {
for (const auto &player : p_game->GetPlayers()) {
NormalizeLabels(player->GetStrategies(), get_label, set_label);
}
}
// Strategy labels are not normalized here either: every strategic-form construction
// path (BuildNfg via RelabelStrategies, and the "1".."N" numbering GameAGGRep/
// GameBAGGRep assign directly) already guarantees unique, nonempty labels per player
// by the time a game reaches here.
}

Game ReadEfgFile(std::istream &p_stream)
Expand Down
28 changes: 14 additions & 14 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -521,8 +521,6 @@ class GameStrategyRep : public std::enable_shared_from_this<GameStrategyRep> {
//@{
/// Returns the text label associated with the strategy
const std::string &GetLabel() const { return m_label; }
/// Sets the text label associated with the strategy
void SetLabel(const std::string &p_label);

/// Returns the game on which the strategy is defined
Game GetGame() const;
Expand Down Expand Up @@ -648,8 +646,10 @@ class GamePlayerRep : public std::enable_shared_from_this<GamePlayerRep> {
GameStrategy GetStrategy(int st) const;
/// Returns the collection of strategies available to the player
Strategies GetStrategies() const;
/// Validate that p_label is a nonempty, valid, unique label for a strategy of this player.
void CheckStrategyLabel(const std::string &p_label) const;
/// Validate that p_label is a valid label for a strategy of this player,
/// disregarding any strategies in p_ignore.
void CheckStrategyLabel(const std::string &p_label,
const std::set<const GameStrategyRep *> &p_ignore) const;
//@}

/// @name Sequences
Expand Down Expand Up @@ -1377,6 +1377,12 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
}
/// 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();
}
/// Returns the total number of actions in the game
virtual int BehavProfileLength() const = 0;
//@}
Expand Down Expand Up @@ -1517,23 +1523,17 @@ inline void GameOutcomeRep::SetPayoff(const GamePlayer &p_player, const Number &

inline GamePlayer GameStrategyRep::GetPlayer() const { return m_player->shared_from_this(); }
inline Game GameStrategyRep::GetGame() const { return m_player->GetGame(); }
inline void GameStrategyRep::SetLabel(const std::string &p_label)
{
if (p_label == m_label) {
return;
}
GetPlayer()->CheckStrategyLabel(p_label);
m_label = p_label;
}

inline void GamePlayerRep::CheckStrategyLabel(const std::string &p_label) const
inline void
GamePlayerRep::CheckStrategyLabel(const std::string &p_label,
const std::set<const GameStrategyRep *> &p_ignore) const
{
if (p_label.empty()) {
throw ValueException("Strategy label must not be empty");
}
CheckLabel(p_label);
for (const auto &strategy : m_strategies) {
if (strategy->GetLabel() == p_label) {
if (p_ignore.count(strategy.get()) == 0 && strategy->GetLabel() == p_label) {
throw ValueException("Strategy label must be unique for the player");
}
}
Expand Down
40 changes: 39 additions & 1 deletion src/games/gametable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ 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) {
Expand Down Expand Up @@ -531,7 +532,7 @@ GameStrategy GameTableRep::NewStrategy(const GamePlayer &p_player, const std::st
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
p_player->CheckStrategyLabel(p_label);
p_player->CheckStrategyLabel(p_label, {});
auto strategy = std::make_shared<GameStrategyRep>(p_player.get(),
p_player->m_strategies.size() + 1, p_label);
IncrementVersion();
Expand Down Expand Up @@ -567,6 +568,43 @@ void GameTableRep::DeleteStrategy(const GameStrategy &p_strategy)
p_strategy->Invalidate();
}

void GameTableRep::RelabelStrategies(const GamePlayer &p_player,
const std::map<std::string, std::string> &p_labels)
{
if (p_player->GetGame().get() != this) {
throw MismatchException();
}
std::map<GameStrategyRep *, std::string> assignment;
std::set<const GameStrategyRep *> relabeled;
for (const auto &[old_label, new_label] : p_labels) {
GameStrategyRep *match = nullptr;
for (const auto &strategy : p_player->m_strategies) {
if (strategy->GetLabel() == old_label) {
if (match) {
throw ValueException("Strategy label '" + old_label + "' is ambiguous for this player");
}
match = strategy.get();
}
}
if (!match) {
throw ValueException("No strategy with label '" + old_label + "' for this player");
}
assignment[match] = new_label;
relabeled.insert(match);
}
std::set<std::string> targets;
for (const auto &[strategy, new_label] : assignment) {
p_player->CheckStrategyLabel(new_label, relabeled);
if (!targets.insert(new_label).second) {
throw ValueException("Strategy label '" + new_label +
"' would be duplicated by the relabelling");
}
}
for (const auto &[strategy, new_label] : assignment) {
strategy->m_label = new_label;
}
}

//------------------------------------------------------------------------
// GameTableRep: Factory functions
//------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions src/games/gametable.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class GameTableRep : public GameExplicitRep {
//@{
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;
//@}

/// @name Writing data files
Expand Down
3 changes: 1 addition & 2 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1819,8 +1819,7 @@ Game GameTreeRep::NormalizeChanceProbs(GameInfosetRep *p_infoset)
throw MismatchException();
}
if (!p_infoset->IsChanceInfoset()) {
throw UndefinedException(
"Action probabilities can only be normalized for chance information sets");
throw UndefinedException("Action probabilities can only be normalized for events");
}
IncrementVersion();
auto &probs = p_infoset->m_probs;
Expand Down
6 changes: 2 additions & 4 deletions src/gui/gamedoc.cc
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,6 @@ GamePlayer GameDocument::DoNewPlayer()
number++;
}
const GamePlayer player = m_game->NewPlayer("Player " + lexical_cast<std::string>(number));
if (!m_game->IsTree()) {
player->GetStrategy(1)->SetLabel("1");
}
NotifyChanged(GameModificationType::GameForm);
return player;
}
Expand Down Expand Up @@ -452,7 +449,8 @@ void GameDocument::DoDeleteStrategy(GameStrategy p_strategy)

void GameDocument::DoSetStrategyLabel(GameStrategy p_strategy, const wxString &p_label)
{
p_strategy->SetLabel(p_label.ToStdString(wxConvUTF8));
m_game->RelabelStrategies(p_strategy->GetPlayer(),
{{p_strategy->GetLabel(), p_label.ToStdString(wxConvUTF8)}});
NotifyChanged(GameModificationType::GameLabels);
}

Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/action.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class Action:
"""
if not self.infoset.is_chance:
raise UndefinedOperationError(
"action probabilities are only defined at chance information sets"
"action probabilities are only defined at events"
)
py_string = cython.cast(
string,
Expand Down
2 changes: 1 addition & 1 deletion src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ cdef extern from "games/game.h":
int GetId() except +
c_GamePlayer GetPlayer() except +
string GetLabel() except +
void SetLabel(string) except +ValueError
c_GameAction GetAction(c_GameInfoset) except +

cdef cppclass c_GameSequenceRep "GameSequenceRep":
Expand Down Expand Up @@ -335,6 +334,7 @@ cdef extern from "games/game.h":
c_GameStrategy GetStrategy(int) except +IndexError
c_GameStrategy NewStrategy(c_GamePlayer, string) except +ValueError
void DeleteStrategy(c_GameStrategy) except +
void RelabelStrategies(c_GamePlayer, stdmap[string, string]) except +ValueError
int MixedProfileLength() except +

c_GameInfoset GetInfoset(int) except +IndexError
Expand Down
Loading
Loading