Skip to content
Open
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
15 changes: 15 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,26 @@
the run. (#998)
- Implemented bespoke XML parser that handles the subset in the de-facto legacy XML workbook .gbt format;
removes dependency on tinyxml. (#897)
- `Game.set_infoset` now requires the node to have the same actions as the target information set, with
the same labels in the same order; previously only the number of actions was checked. (#999)
- `Game.set_infoset` now raises when called on a terminal node or a chance node; previously a terminal
node was silently ignored. (#999)
- `Game.reveal` now raises at absent-minded information sets; previously the result was undefined. (#999)

### Added
- Added `Game.make_infoset`, forming a collection of personal decision nodes into a single
information set; the primitive operation for editing information structures. (#999)

### 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.

### Removed
- `Game.set_player` has been removed; use `Game.make_infoset`, specifying the members of the
information set and the new player. (#999)
- `Game.sort_infosets` has been removed; it was deprecated in 16.5.0, as the iteration order of
information sets and their members is now maintained automatically. (#999)

## [16.7.0] - 2026-07-11

### Added
Expand Down
3 changes: 1 addition & 2 deletions doc/pygambit.api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,11 @@ Transforming game information structure
.. autosummary::
:toctree: api/

Game.set_player
Game.make_infoset
Game.set_infoset
Game.leave_infoset
Game.set_chance_probs
Game.reveal
Game.sort_infosets


Transforming game components
Expand Down
37 changes: 27 additions & 10 deletions src/games/game.h
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,11 @@ class GamePlayerRep : public std::enable_shared_from_this<GamePlayerRep> {
GameInfoset GetInfoset(int p_index) const;
/// Returns the information sets for the player
Infosets GetInfosets() const;
/// Validate that p_label is a valid label for an information set of this player,
/// disregarding any information sets in p_ignore.
void CheckInfosetLabel(const std::string &p_label,
const std::set<const GameInfosetRep *> &p_ignore) const;
//@}

/// @name Strategies
//@{
Expand Down Expand Up @@ -1103,6 +1108,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
virtual void Reveal(GameInfoset, GamePlayer) { throw UndefinedException(); }
virtual void SetInfoset(GameNode, GameInfoset) { throw UndefinedException(); }
virtual GameInfoset LeaveInfoset(GameNode) { throw UndefinedException(); }
virtual GameInfoset MakeInfoset(const std::vector<GameNode> &, const GamePlayer &,
const std::string &)
{
throw UndefinedException();
}
virtual GameAction InsertAction(GameInfoset, GameAction p_where = nullptr)
{
throw UndefinedException();
Expand Down Expand Up @@ -1338,6 +1348,22 @@ inline void GamePlayerRep::CheckStrategyLabel(const std::string &p_label) const
}
}

inline void
GamePlayerRep::CheckInfosetLabel(const std::string &p_label,
const std::set<const GameInfosetRep *> &p_ignore) const
{
CheckLabel(p_label);
// Infoset labels may be empty; a nonempty label must be unique among the infosets of the player.
if (p_label.empty()) {
return;
}
for (const auto &infoset : m_infosets) {
if (p_ignore.count(infoset.get()) == 0 && infoset->GetLabel() == p_label) {
throw ValueException("Infoset label must be unique for the player");
}
}
}

inline Game GameSequenceRep::GetGame() const { return m_player->GetGame(); }
inline GamePlayer GameSequenceRep::GetPlayer() const { return m_player->shared_from_this(); }

Expand All @@ -1350,16 +1376,7 @@ inline void GameInfosetRep::SetLabel(const std::string &p_label)
if (p_label == m_label) {
return;
}
CheckLabel(p_label);
// Infoset labels may be empty, but a non-empty label must be unique among
// the infosets of the same player.
if (!p_label.empty()) {
for (const auto &infoset : GetPlayer()->GetInfosets()) {
if (infoset.get() != this && infoset->GetLabel() == p_label) {
throw ValueException("Infoset label must be unique for the player");
}
}
}
m_player->CheckInfosetLabel(p_label, {this});
m_label = p_label;
}
inline void GameRep::CheckPlayerLabel(const std::string &p_label) const
Expand Down
75 changes: 75 additions & 0 deletions src/games/gametree.cc
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,81 @@ Game GameTreeRep::CopySubgame(GameNode p_root) const
return ReadGame(is);
}

GameInfoset GameTreeRep::MakeInfoset(const std::vector<GameNode> &p_nodes,
const GamePlayer &p_player, const std::string &p_label)
{
if (p_nodes.empty()) {
throw ValueException("At least one node must be specified");
}
if (p_player->m_game != this) {
throw MismatchException();
}
if (p_player->IsChance()) {
throw UndefinedException("The information set must belong to a personal player, not chance");
}
std::set<GameNodeRep *> selected;
for (const auto &node : p_nodes) {
if (node->m_game != this) {
throw MismatchException();
}
if (node->IsTerminal()) {
throw UndefinedException("All nodes must be decision nodes");
}
if (node->m_infoset->IsChanceInfoset()) {
throw UndefinedException("All nodes must belong to a personal player, not chance");
}
if (!selected.insert(node.get()).second) {
throw ValueException("Each node may be referenced only once");
}
}
// The first member defines the action labels and their order of the new information set.
const auto &reference = p_nodes.front()->m_infoset->m_actions;
for (const auto &node : p_nodes) {
const auto &actions = node->m_infoset->m_actions;
if (!std::equal(
actions.begin(), actions.end(), reference.begin(), reference.end(),
[](const std::shared_ptr<GameActionRep> &a, const std::shared_ptr<GameActionRep> &b) {
return a->GetLabel() == b->GetLabel();
})) {
throw ValueException(
"All nodes must have the same actions, with the same labels in the same order");
}
}
// Destroy an infoset all of whose members are absorbed; the label it holds can then be reused.
std::set<const GameInfosetRep *> absorbed;
if (!p_label.empty()) {
for (const auto &infoset : p_player->m_infosets) {
if (infoset->GetLabel() == p_label &&
std::all_of(infoset->m_members.begin(), infoset->m_members.end(),
[&selected](const std::shared_ptr<GameNodeRep> &m) {
return contains(selected, m.get());
})) {
absorbed.insert(infoset.get());
}
}
}
p_player->CheckInfosetLabel(p_label, absorbed);

IncrementVersion();
auto newInfoset = std::make_shared<GameInfosetRep>(this, p_player->m_infosets.size() + 1,
p_player.get(), reference.size());
auto dest = newInfoset->m_actions.begin();
for (const auto &action : reference) {
(*dest)->SetLabel(action->GetLabel());
++dest;
}
p_player->m_infosets.push_back(newInfoset);
for (const auto &node : p_nodes) {
RemoveMember(node->m_infoset, node.get());
newInfoset->m_members.push_back(node);
node->m_infoset = newInfoset.get();
}
newInfoset->SetLabel(p_label);
ClearComputedValues();
InvalidateInfosetOrdering();
return newInfoset;
}

void GameTreeRep::SetInfoset(GameNode p_node, GameInfoset p_infoset)
{
if (p_node->m_game != this || p_infoset->m_game != this) {
Expand Down
2 changes: 2 additions & 0 deletions src/games/gametree.h
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ class GameTreeRep final : public GameExplicitRep {
void DeleteTree(GameNode) override;
void SetPlayer(GameInfoset, GamePlayer) override;
void Reveal(GameInfoset, GamePlayer) override;
GameInfoset MakeInfoset(const std::vector<GameNode> &, const GamePlayer &,
const std::string &) override;
void SetInfoset(GameNode, GameInfoset) override;
GameInfoset LeaveInfoset(GameNode) override;
Game SetChanceProbs(const GameInfoset &, const Array<Number> &) override;
Expand Down
1 change: 1 addition & 0 deletions src/pygambit/gambit.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ cdef extern from "games/game.h":
void DeleteTree(c_GameNode) except +
void SetPlayer(c_GameInfoset, c_GamePlayer) except +
void Reveal(c_GameInfoset, c_GamePlayer) except +
c_GameInfoset MakeInfoset(stdvector[c_GameNode], c_GamePlayer, string) except +ValueError
void SetInfoset(c_GameNode, c_GameInfoset) except +ValueError
c_GameInfoset LeaveInfoset(c_GameNode) except +
c_GameAction InsertAction(c_GameInfoset, c_GameAction) except +ValueError
Expand Down
Loading
Loading