From 45d326aa69d4895802380b4c8be1470ab6871d4c Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 21:12:25 -0400 Subject: [PATCH 1/6] Define concept of PureSequenceProfile and replace previous ad-hoc std::map. --- Makefile.am | 2 + src/gambit.h | 1 + src/games/behavspt.cc | 19 ++++----- src/games/behavspt.h | 7 ++-- src/games/gameseq.cc | 14 +++---- src/games/gameseq.h | 15 ++++--- src/games/gametree.cc | 13 +++--- src/games/gametree.h | 4 +- src/games/seqpure.cc | 29 +++++++++++++ src/games/seqpure.h | 72 +++++++++++++++++++++++++++++++++ src/solvers/enumpoly/efgpoly.cc | 2 +- 11 files changed, 139 insertions(+), 39 deletions(-) create mode 100644 src/games/seqpure.cc create mode 100644 src/games/seqpure.h diff --git a/Makefile.am b/Makefile.am index 8f8bd8e914..835e0f0e97 100644 --- a/Makefile.am +++ b/Makefile.am @@ -261,6 +261,8 @@ game_SOURCES = \ src/games/behavpure.h \ src/games/behavmixed.cc \ src/games/behavmixed.h \ + src/games/seqpure.cc \ + src/games/seqpure.h \ src/games/stratspt.cc \ src/games/stratspt.h \ src/games/stratpure.h \ diff --git a/src/gambit.h b/src/gambit.h index 6cece1946b..e60909e6c6 100644 --- a/src/gambit.h +++ b/src/gambit.h @@ -31,6 +31,7 @@ #include "games/behavspt.h" #include "games/behavmixed.h" #include "games/behavpure.h" +#include "games/seqpure.h" #include "games/stratspt.h" #include "games/stratpure.h" diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 7f73c093a2..bdf4ed0e91 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -169,9 +169,8 @@ int BehaviorSupportProfile::GetConstraintEntry(const GameInfoset &p_infoset, return GetSequenceForm()->GetConstraintEntry(p_infoset, p_action); } -const Rational & -BehaviorSupportProfile::GetPayoff(const std::map &p_profile, - const GamePlayer &p_player) const +const Rational &BehaviorSupportProfile::GetPayoff(const PureSequenceProfile &p_profile, + const GamePlayer &p_player) const { return GetSequenceForm()->GetPayoff(p_profile, p_player); } @@ -284,22 +283,20 @@ BehaviorSupportProfile::SequenceContingencies::iterator::iterator( } } -std::map -BehaviorSupportProfile::SequenceContingencies::iterator::operator*() const +PureSequenceProfile BehaviorSupportProfile::SequenceContingencies::iterator::operator*() const { - std::map ret; + PureSequenceProfile ret(m_sfg->GetSupport().GetGame()); for (auto [player, index] : m_indices) { - ret[player] = m_sfg->m_sequences.at(player)[index]; + ret.SetSequence(m_sfg->m_sequences.at(player)[index]); } return ret; } -std::map -BehaviorSupportProfile::SequenceContingencies::iterator::operator->() const +PureSequenceProfile BehaviorSupportProfile::SequenceContingencies::iterator::operator->() const { - std::map ret; + PureSequenceProfile ret(m_sfg->GetSupport().GetGame()); for (auto [player, index] : m_indices) { - ret[player] = m_sfg->m_sequences.at(player)[index]; + ret.SetSequence(m_sfg->m_sequences.at(player)[index]); } return ret; } diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 9f72af66c8..bbd047b5d4 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -26,6 +26,7 @@ #include #include #include "game.h" +#include "seqpure.h" namespace Gambit { @@ -212,9 +213,9 @@ class BehaviorSupportProfile { iterator(const std::shared_ptr p_sfg, bool p_end = false); - std::map operator*() const; + PureSequenceProfile operator*() const; - std::map operator->() const; + PureSequenceProfile operator->() const; iterator &operator++(); @@ -233,7 +234,7 @@ class BehaviorSupportProfile { Sequences GetSequences() const; PlayerSequences GetSequences(const GamePlayer &p_player) const; int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; - const Rational &GetPayoff(const std::map &p_profile, + const Rational &GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } MixedBehaviorProfile diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index dc149fb146..e66a7abc57 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -42,7 +42,7 @@ void GameSequenceForm::BuildSequences() } void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, - std::map &p_currentSequences) + PureSequenceProfile &p_currentSequences) { if (n->GetOutcome()) { for (auto player : m_support.GetGame()->GetPlayers()) { @@ -61,14 +61,14 @@ void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, } } else { - auto tmp_sequence = p_currentSequences.at(n->GetPlayer()); - m_constraints[{n->GetInfoset(), p_currentSequences.at(n->GetPlayer())->GetAction()}] = 1; + auto tmp_sequence = p_currentSequences.GetSequence(n->GetPlayer()); + m_constraints[{n->GetInfoset(), tmp_sequence->GetAction()}] = 1; for (auto action : m_support.GetActions(n->GetInfoset())) { m_constraints[{n->GetInfoset(), action}] = -1; - p_currentSequences[n->GetPlayer()] = m_correspondence.at(action); + p_currentSequences.SetSequence(m_correspondence.at(action)); FillTableau(n->GetChild(action), prob, p_currentSequences); } - p_currentSequences[n->GetPlayer()] = tmp_sequence; + p_currentSequences.SetSequence(tmp_sequence); } } @@ -80,9 +80,9 @@ void GameSequenceForm::FillTableau() } m_payoffs = NDArray(dim, dim.size()); - std::map currentSequence; + PureSequenceProfile currentSequence(m_support.GetGame()); for (auto player : GetPlayers()) { - currentSequence[player] = m_sequences[player].front(); + currentSequence.SetSequence(m_sequences[player].front()); } FillTableau(m_support.GetGame()->GetRoot(), Rational(1), currentSequence); } diff --git a/src/games/gameseq.h b/src/games/gameseq.h index 80678d1128..6a61c51ce7 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -25,6 +25,7 @@ #include "gambit.h" #include "ndarray.h" +#include "seqpure.h" namespace Gambit { @@ -40,21 +41,20 @@ class GameSequenceForm { void BuildSequences(); void FillTableau(); - void FillTableau(const GameNode &, const Rational &, std::map &); + void FillTableau(const GameNode &, const Rational &, PureSequenceProfile &); - Array ProfileToIndex(const std::map &p_profile) const + Array ProfileToIndex(const PureSequenceProfile &p_profile) const { - Array index(p_profile.size()); + Array index(GetPlayers().size()); for (auto player : GetPlayers()) { const auto &seqs = m_sequences.at(player); - auto loc = std::find(seqs.begin(), seqs.end(), p_profile.at(player)); + auto loc = std::find(seqs.begin(), seqs.end(), p_profile.GetSequence(player)); index[player->GetNumber()] = loc - seqs.begin() + 1; } return index; } - Rational &GetPayoffEntry(const std::map &p_profile, - const GamePlayer &p_player) + Rational &GetPayoffEntry(const PureSequenceProfile &p_profile, const GamePlayer &p_player) { return m_payoffs.at(ProfileToIndex(p_profile), p_player->GetNumber()); } @@ -81,8 +81,7 @@ class GameSequenceForm { GameRep::Players GetPlayers() const { return m_support.GetGame()->GetPlayers(); } - const Rational &GetPayoff(const std::map &p_profile, - const GamePlayer &p_player) const + const Rational &GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const { return m_payoffs.at(ProfileToIndex(p_profile), p_player->GetNumber()); } diff --git a/src/games/gametree.cc b/src/games/gametree.cc index 84d6ec0fd1..ba285d8dd4 100644 --- a/src/games/gametree.cc +++ b/src/games/gametree.cc @@ -997,8 +997,7 @@ void GameTreeRep::BuildComputedValues() const m_computedValues = true; } -void GameTreeRep::BuildSequences(const GameNode &n, - std::map &p_currentSequences) const +void GameTreeRep::BuildSequences(const GameNode &n, PureSequenceProfile &p_currentSequences) const { if (!n->GetInfoset()) { return; @@ -1010,7 +1009,7 @@ void GameTreeRep::BuildSequences(const GameNode &n, } else { auto *player = n->m_infoset->m_player; - const auto tmp_sequence = p_currentSequences.at(n->GetPlayer()); + const auto tmp_sequence = p_currentSequences.GetSequence(n->GetPlayer()); for (const auto &action : n->m_infoset->m_actions) { auto seq_it = std::find_if(player->m_sequences.begin(), player->m_sequences.end(), @@ -1025,10 +1024,10 @@ void GameTreeRep::BuildSequences(const GameNode &n, else { sequence = *seq_it; } - p_currentSequences[n->GetPlayer()] = sequence; + p_currentSequences.SetSequence(sequence); BuildSequences(n->GetChild(action), p_currentSequences); } - p_currentSequences[n->GetPlayer()] = tmp_sequence; + p_currentSequences.SetSequence(tmp_sequence); } } @@ -1037,11 +1036,11 @@ void GameTreeRep::EnsureSequences() const if (m_hasSequences) { return; } - std::map currentSequences; + PureSequenceProfile currentSequences(m_root->GetGame()); for (const auto &player : m_players) { player->m_sequences = {std::make_shared(player.get(), nullptr, 1, std::weak_ptr())}; - currentSequences[player] = player->m_sequences.front(); + currentSequences.SetSequence(player->m_sequences.front()); } BuildSequences(m_root, currentSequences); m_hasSequences = true; diff --git a/src/games/gametree.h b/src/games/gametree.h index 1b2e15e9e5..f2861fbf23 100644 --- a/src/games/gametree.h +++ b/src/games/gametree.h @@ -24,6 +24,7 @@ #define GAMETREE_H #include "gameexpl.h" +#include "seqpure.h" #include namespace Gambit { @@ -98,8 +99,7 @@ class GameTreeRep final : public GameExplicitRep { void ClearComputedValues() const; void EnsureSequences() const override; - void BuildSequences(const GameNode &n, - std::map &p_currentSequences) const; + void BuildSequences(const GameNode &n, PureSequenceProfile &p_currentSequences) const; /// Removes the node from the information set, invalidating if emptied void RemoveMember(GameInfosetRep *, GameNodeRep *); diff --git a/src/games/seqpure.cc b/src/games/seqpure.cc new file mode 100644 index 0000000000..5c7d1f5e4e --- /dev/null +++ b/src/games/seqpure.cc @@ -0,0 +1,29 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/games/seqpure.cc +// Implementation of pure sequence profile +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#include "gambit.h" + +namespace Gambit { + +PureSequenceProfile::PureSequenceProfile(const Game &p_efg) : m_efg(p_efg) {} + +} // end namespace Gambit diff --git a/src/games/seqpure.h b/src/games/seqpure.h new file mode 100644 index 0000000000..9fc6087079 --- /dev/null +++ b/src/games/seqpure.h @@ -0,0 +1,72 @@ +// +// This file is part of Gambit +// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) +// +// FILE: src/games/seqpure.h +// Declaration of pure sequence profile +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +// + +#ifndef GAMBIT_GAMES_SEQPURE_H +#define GAMBIT_GAMES_SEQPURE_H + +#include +#include "game.h" + +namespace Gambit { + +/// This class represents an assignment of one sequence to each (personal) +/// player of an extensive game. It is the analogue, for the sequence form, +/// of PureStrategyProfile for the strategic form and PureBehaviorProfile +/// for the behavior representation of the extensive form. +class PureSequenceProfile { + Game m_efg; + std::map m_profile; + +public: + /// @name Lifecycle + //@{ + /// Construct a new sequence profile on the specified game. No sequence + /// is assigned to any player until SetSequence() is called. + explicit PureSequenceProfile(const Game &); + //@} + + Game GetGame() const { return m_efg; } + + bool operator==(const PureSequenceProfile &p_other) const + { + return m_profile == p_other.m_profile; + } + bool operator!=(const PureSequenceProfile &p_other) const { return !(*this == p_other); } + + /// @name Data access and manipulation + //@{ + /// Get the sequence assigned to the player + const GameSequence &GetSequence(const GamePlayer &p_player) const + { + return m_profile.at(p_player); + } + /// Assign the sequence to (its) player + void SetSequence(const GameSequence &p_sequence) + { + m_profile[p_sequence->GetPlayer()] = p_sequence; + } + //@} +}; + +} // end namespace Gambit + +#endif // GAMBIT_GAMES_SEQPURE_H diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 3a0e1029e5..38f62d51d0 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -105,7 +105,7 @@ Polynomial GetPayoff(ProblemData &p_data, const GamePlayer &p_player) if (pay != Rational(0)) { Polynomial term(p_data.space, double(pay)); for (auto player : p_data.m_support.GetPlayers()) { - term *= p_data.variables.at(profile[player]); + term *= p_data.variables.at(profile.GetSequence(player)); } equation += term; } From 8e239fed30c07941f89adea0b0fba1768c953e9f Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 21:33:22 -0400 Subject: [PATCH 2/6] Remove realisation of full sequence form payoff table in favour of on-demand calculation. On-demand calculation is stack-based rather than recursive. --- src/games/behavspt.cc | 4 +- src/games/behavspt.h | 3 +- src/games/gameseq.cc | 126 ++++++++++++++++++++++++++++++------------ src/games/gameseq.h | 43 +++----------- 4 files changed, 103 insertions(+), 73 deletions(-) diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index bdf4ed0e91..05fcfffefd 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -169,8 +169,8 @@ int BehaviorSupportProfile::GetConstraintEntry(const GameInfoset &p_infoset, return GetSequenceForm()->GetConstraintEntry(p_infoset, p_action); } -const Rational &BehaviorSupportProfile::GetPayoff(const PureSequenceProfile &p_profile, - const GamePlayer &p_player) const +Rational BehaviorSupportProfile::GetPayoff(const PureSequenceProfile &p_profile, + const GamePlayer &p_player) const { return GetSequenceForm()->GetPayoff(p_profile, p_player); } diff --git a/src/games/behavspt.h b/src/games/behavspt.h index bbd047b5d4..097fefe54c 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -234,8 +234,7 @@ class BehaviorSupportProfile { Sequences GetSequences() const; PlayerSequences GetSequences(const GamePlayer &p_player) const; int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; - const Rational &GetPayoff(const PureSequenceProfile &p_profile, - const GamePlayer &p_player) const; + Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } MixedBehaviorProfile ToMixedBehaviorProfile(const std::map &) const; diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index e66a7abc57..d5555c90dc 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -20,6 +20,8 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include + #include "gambit.h" #include "gameseq.h" @@ -41,50 +43,106 @@ void GameSequenceForm::BuildSequences() } } -void GameSequenceForm::FillTableau(const GameNode &n, const Rational &prob, - PureSequenceProfile &p_currentSequences) +// Because information sets respect a player's own history, every action at +// a given (reachable) information set is reached via the same sequence of +// that player's prior moves. This means the constraint matrix -- which +// relates the probability of a sequence to the probabilities of the +// sequences that extend it -- can be read off directly from the sequences' +// parent relationships, without walking the tree. +void GameSequenceForm::BuildConstraints() { - if (n->GetOutcome()) { - for (auto player : m_support.GetGame()->GetPlayers()) { - GetPayoffEntry(p_currentSequences, player) += - prob * n->GetOutcome()->GetPayoff(player); - } - } - if (!n->GetInfoset()) { - return; - } - if (n->GetPlayer()->IsChance()) { - for (auto action : n->GetInfoset()->GetActions()) { - FillTableau(n->GetChild(action), - prob * static_cast(n->GetInfoset()->GetActionProb(action)), - p_currentSequences); - } - } - else { - auto tmp_sequence = p_currentSequences.GetSequence(n->GetPlayer()); - m_constraints[{n->GetInfoset(), tmp_sequence->GetAction()}] = 1; - for (auto action : m_support.GetActions(n->GetInfoset())) { - m_constraints[{n->GetInfoset(), action}] = -1; - p_currentSequences.SetSequence(m_correspondence.at(action)); - FillTableau(n->GetChild(action), prob, p_currentSequences); + for (const auto &player : GetPlayers()) { + for (const auto &infoset : player->GetInfosets()) { + if (!m_support.IsReachable(infoset)) { + continue; + } + const auto &actions = m_support.GetActions(infoset); + const GameAction arrival = m_correspondence.at(actions.front())->GetParent()->GetAction(); + m_constraints[{infoset, arrival}] = 1; + for (const auto &action : actions) { + m_constraints[{infoset, action}] = -1; + } } - p_currentSequences.SetSequence(tmp_sequence); } } -void GameSequenceForm::FillTableau() +namespace { + +/// One frame of the explicit-stack traversal used by GameSequenceForm::GetPayoff. +/// Tracks the node under consideration, the probability of reaching it (given +/// chance's actual probabilities and each player's moves as prescribed by the +/// profile being evaluated), and how far each player has progressed along the +/// chain of sequences from the empty sequence to the one designated for them. +struct SequenceWalkFrame { + GameNode node; + Rational prob; + std::map progress; +}; + +} // end anonymous namespace + +Rational GameSequenceForm::GetPayoff(const PureSequenceProfile &p_profile, + const GamePlayer &p_player) const { - Array dim(m_sequences.size()); + std::map> chains; + std::map initialProgress; for (auto player : GetPlayers()) { - dim[player->GetNumber()] = m_sequences.at(player).size(); + std::vector chain; + for (GameSequence seq = p_profile.GetSequence(player); seq; seq = seq->GetParent()) { + chain.push_back(seq); + } + std::reverse(chain.begin(), chain.end()); // chain.front() is the empty sequence + chains[player] = chain; + initialProgress[player] = 0; } - m_payoffs = NDArray(dim, dim.size()); - PureSequenceProfile currentSequence(m_support.GetGame()); - for (auto player : GetPlayers()) { - currentSequence.SetSequence(m_sequences[player].front()); + Rational payoff(0); + std::stack frames; + frames.push({m_support.GetGame()->GetRoot(), Rational(1), initialProgress}); + + while (!frames.empty()) { + const SequenceWalkFrame frame = std::move(frames.top()); + frames.pop(); + const GameNode &n = frame.node; + + if (n->GetOutcome()) { + const bool matches = std::all_of(chains.begin(), chains.end(), [&](const auto &entry) { + return frame.progress.at(entry.first) + 1 == entry.second.size(); + }); + if (matches) { + payoff += frame.prob * n->GetOutcome()->GetPayoff(p_player); + } + } + if (!n->GetInfoset()) { + continue; + } + if (n->GetPlayer()->IsChance()) { + for (auto action : n->GetInfoset()->GetActions()) { + frames.push({n->GetChild(action), + frame.prob * static_cast(n->GetInfoset()->GetActionProb(action)), + frame.progress}); + } + continue; + } + + const auto &chain = chains.at(n->GetPlayer()); + const size_t index = frame.progress.at(n->GetPlayer()); + if (index + 1 >= chain.size()) { + // This player has already realised their designated sequence; any + // further move of theirs here is inconsistent with this profile. + continue; + } + const GameSequence &next = chain[index + 1]; + if (next->GetInfoset() != n->GetInfoset()) { + // This is not the information set at which this player's next + // designated move occurs; this branch cannot realise the profile. + continue; + } + auto progress = frame.progress; + progress[n->GetPlayer()] = index + 1; + frames.push({n->GetChild(next->GetAction()), frame.prob, std::move(progress)}); } - FillTableau(m_support.GetGame()->GetRoot(), Rational(1), currentSequence); + return payoff; } } // end namespace Gambit diff --git a/src/games/gameseq.h b/src/games/gameseq.h index 6a61c51ce7..e680cc6de5 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -24,7 +24,6 @@ #define GAMESEQ_H #include "gambit.h" -#include "ndarray.h" #include "seqpure.h" namespace Gambit { @@ -34,45 +33,17 @@ class GameSequenceForm { BehaviorSupportProfile m_support; std::map> m_sequences; - NDArray m_payoffs; std::map, int> m_constraints; // (sparse) constraint matrices - std::set m_infosets; // infosets actually reachable given support std::map m_correspondence; void BuildSequences(); - void FillTableau(); - void FillTableau(const GameNode &, const Rational &, PureSequenceProfile &); - - Array ProfileToIndex(const PureSequenceProfile &p_profile) const - { - Array index(GetPlayers().size()); - for (auto player : GetPlayers()) { - const auto &seqs = m_sequences.at(player); - auto loc = std::find(seqs.begin(), seqs.end(), p_profile.GetSequence(player)); - index[player->GetNumber()] = loc - seqs.begin() + 1; - } - return index; - } - - Rational &GetPayoffEntry(const PureSequenceProfile &p_profile, const GamePlayer &p_player) - { - return m_payoffs.at(ProfileToIndex(p_profile), p_player->GetNumber()); - } + void BuildConstraints(); public: - class Infosets { - const GameSequenceForm *m_sfg; - - public: - Infosets(const GameSequenceForm *p_sfg) : m_sfg(p_sfg) {} - - size_t size() const { return m_sfg->m_infosets.size(); } - }; - explicit GameSequenceForm(const BehaviorSupportProfile &p_support) : m_support(p_support) { BuildSequences(); - FillTableau(); + BuildConstraints(); } ~GameSequenceForm() = default; @@ -81,10 +52,12 @@ class GameSequenceForm { GameRep::Players GetPlayers() const { return m_support.GetGame()->GetPlayers(); } - const Rational &GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const - { - return m_payoffs.at(ProfileToIndex(p_profile), p_player->GetNumber()); - } + /// Returns the payoff to a player that arises when each player realises + /// the sequence assigned to them in the profile. This is computed on + /// demand by an (iterative, non-recursive) traversal of the game tree, + /// pruning as soon as a player's move is inconsistent with their + /// designated sequence. + Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const { From 167e42bcc27767853028647c4bde156e1efb1ae8 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 21:56:25 -0400 Subject: [PATCH 3/6] Remove "constraint matrix" from sequence form class, and just generate in enumpoly in the format appropriate to the problem formulation there --- src/games/behavspt.cc | 6 ------ src/games/behavspt.h | 1 - src/games/gameseq.cc | 26 -------------------------- src/games/gameseq.h | 14 -------------- src/solvers/enumpoly/efgpoly.cc | 32 +++++++++++++++++++++----------- 5 files changed, 21 insertions(+), 58 deletions(-) diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 05fcfffefd..187804f42c 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -163,12 +163,6 @@ BehaviorSupportProfile::GetSequences(const GamePlayer &p_player) const return {this, p_player}; } -int BehaviorSupportProfile::GetConstraintEntry(const GameInfoset &p_infoset, - const GameAction &p_action) const -{ - return GetSequenceForm()->GetConstraintEntry(p_infoset, p_action); -} - Rational BehaviorSupportProfile::GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const { diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 097fefe54c..24c1e19b8f 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -233,7 +233,6 @@ class BehaviorSupportProfile { std::shared_ptr GetSequenceForm() const; Sequences GetSequences() const; PlayerSequences GetSequences(const GamePlayer &p_player) const; - int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const; Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } MixedBehaviorProfile diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index d5555c90dc..a8c7cc4531 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -35,32 +35,6 @@ void GameSequenceForm::BuildSequences() for (const auto &sequence : player->GetSequences()) { if (!sequence->GetAction() || m_support.Contains(sequence->GetAction())) { m_sequences[player].emplace_back(sequence); - if (sequence->GetAction()) { - m_correspondence[sequence->GetAction()] = sequence; - } - } - } - } -} - -// Because information sets respect a player's own history, every action at -// a given (reachable) information set is reached via the same sequence of -// that player's prior moves. This means the constraint matrix -- which -// relates the probability of a sequence to the probabilities of the -// sequences that extend it -- can be read off directly from the sequences' -// parent relationships, without walking the tree. -void GameSequenceForm::BuildConstraints() -{ - for (const auto &player : GetPlayers()) { - for (const auto &infoset : player->GetInfosets()) { - if (!m_support.IsReachable(infoset)) { - continue; - } - const auto &actions = m_support.GetActions(infoset); - const GameAction arrival = m_correspondence.at(actions.front())->GetParent()->GetAction(); - m_constraints[{infoset, arrival}] = 1; - for (const auto &action : actions) { - m_constraints[{infoset, action}] = -1; } } } diff --git a/src/games/gameseq.h b/src/games/gameseq.h index e680cc6de5..a4e6b95103 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -33,17 +33,13 @@ class GameSequenceForm { BehaviorSupportProfile m_support; std::map> m_sequences; - std::map, int> m_constraints; // (sparse) constraint matrices - std::map m_correspondence; void BuildSequences(); - void BuildConstraints(); public: explicit GameSequenceForm(const BehaviorSupportProfile &p_support) : m_support(p_support) { BuildSequences(); - BuildConstraints(); } ~GameSequenceForm() = default; @@ -58,16 +54,6 @@ class GameSequenceForm { /// pruning as soon as a player's move is inconsistent with their /// designated sequence. Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; - - int GetConstraintEntry(const GameInfoset &p_infoset, const GameAction &p_action) const - { - try { - return m_constraints.at({p_infoset, p_action}); - } - catch (std::out_of_range &) { - return 0; - } - } }; } // end namespace Gambit diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 38f62d51d0..34d0af9832 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -52,6 +52,14 @@ class ProblemData { std::shared_ptr space; std::map var; std::map> variables; + // The sequences of the actions available at an information set, i.e. the + // children, in the tree of sequences, of the sequence that leads to it. + // (Grouping by information set rather than directly by parent sequence + // matters: the same parent sequence can lead to different information + // sets, e.g. depending on an intervening chance move or another player's + // action.) This is what expresses the sum-to-one relation among the + // actions at an information set. + std::map> siblings; explicit ProblemData(const BehaviorSupportProfile &p_support); }; @@ -66,14 +74,14 @@ Polynomial BuildSequenceVariable(ProblemData &p_data, const GameSequence return Polynomial(p_data.space, var.at(p_sequence), 1); } - Polynomial equation(p_data.space); - for (auto seq : p_data.m_support.GetSequences(p_sequence->GetPlayer())) { - if (seq == p_sequence) { - continue; - } - if (const int constraint_coef = - p_data.m_support.GetConstraintEntry(p_sequence->GetInfoset(), seq->GetAction())) { - equation += BuildSequenceVariable(p_data, seq, var) * double(constraint_coef); + // The last action at an information set is eliminated using the + // sum-to-one relation among the sequences at that information set: its + // probability is that of its parent sequence, less the probabilities of + // its sibling sequences (the other actions at the same information set). + Polynomial equation = BuildSequenceVariable(p_data, p_sequence->GetParent(), var); + for (const auto &sibling : p_data.siblings.at(p_sequence->GetInfoset())) { + if (sibling != p_sequence) { + equation -= BuildSequenceVariable(p_data, sibling, var); } } return equation; @@ -85,9 +93,11 @@ ProblemData::ProblemData(const BehaviorSupportProfile &p_support) m_support.GetPlayers().size())) { for (auto sequence : m_support.GetSequences()) { - if (sequence->GetAction() && - (sequence->GetAction() != p_support.GetActions(sequence->GetInfoset()).back())) { - var[sequence] = var.size() + 1; + if (sequence->GetAction()) { + siblings[sequence->GetInfoset()].push_back(sequence); + if (sequence->GetAction() != p_support.GetActions(sequence->GetInfoset()).back()) { + var[sequence] = var.size() + 1; + } } } From f492cfadd543185bff4c31a95d6b362a3862dc0d Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 22:07:12 -0400 Subject: [PATCH 4/6] Move GetPayoff to the puresequenceprofile --- src/games/behavspt.cc | 6 --- src/games/behavspt.h | 1 - src/games/gameseq.cc | 81 --------------------------------- src/games/gameseq.h | 7 --- src/games/seqpure.cc | 81 +++++++++++++++++++++++++++++++++ src/games/seqpure.h | 7 +++ src/solvers/enumpoly/efgpoly.cc | 2 +- 7 files changed, 89 insertions(+), 96 deletions(-) diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 187804f42c..5279a3c09c 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -163,12 +163,6 @@ BehaviorSupportProfile::GetSequences(const GamePlayer &p_player) const return {this, p_player}; } -Rational BehaviorSupportProfile::GetPayoff(const PureSequenceProfile &p_profile, - const GamePlayer &p_player) const -{ - return GetSequenceForm()->GetPayoff(p_profile, p_player); -} - BehaviorSupportProfile::SequenceContingencies BehaviorSupportProfile::GetSequenceContingencies() const { diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 24c1e19b8f..4c5ae878ee 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -233,7 +233,6 @@ class BehaviorSupportProfile { std::shared_ptr GetSequenceForm() const; Sequences GetSequences() const; PlayerSequences GetSequences(const GamePlayer &p_player) const; - Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } MixedBehaviorProfile ToMixedBehaviorProfile(const std::map &) const; diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc index a8c7cc4531..24b8470910 100644 --- a/src/games/gameseq.cc +++ b/src/games/gameseq.cc @@ -20,8 +20,6 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // -#include - #include "gambit.h" #include "gameseq.h" @@ -40,83 +38,4 @@ void GameSequenceForm::BuildSequences() } } -namespace { - -/// One frame of the explicit-stack traversal used by GameSequenceForm::GetPayoff. -/// Tracks the node under consideration, the probability of reaching it (given -/// chance's actual probabilities and each player's moves as prescribed by the -/// profile being evaluated), and how far each player has progressed along the -/// chain of sequences from the empty sequence to the one designated for them. -struct SequenceWalkFrame { - GameNode node; - Rational prob; - std::map progress; -}; - -} // end anonymous namespace - -Rational GameSequenceForm::GetPayoff(const PureSequenceProfile &p_profile, - const GamePlayer &p_player) const -{ - std::map> chains; - std::map initialProgress; - for (auto player : GetPlayers()) { - std::vector chain; - for (GameSequence seq = p_profile.GetSequence(player); seq; seq = seq->GetParent()) { - chain.push_back(seq); - } - std::reverse(chain.begin(), chain.end()); // chain.front() is the empty sequence - chains[player] = chain; - initialProgress[player] = 0; - } - - Rational payoff(0); - std::stack frames; - frames.push({m_support.GetGame()->GetRoot(), Rational(1), initialProgress}); - - while (!frames.empty()) { - const SequenceWalkFrame frame = std::move(frames.top()); - frames.pop(); - const GameNode &n = frame.node; - - if (n->GetOutcome()) { - const bool matches = std::all_of(chains.begin(), chains.end(), [&](const auto &entry) { - return frame.progress.at(entry.first) + 1 == entry.second.size(); - }); - if (matches) { - payoff += frame.prob * n->GetOutcome()->GetPayoff(p_player); - } - } - if (!n->GetInfoset()) { - continue; - } - if (n->GetPlayer()->IsChance()) { - for (auto action : n->GetInfoset()->GetActions()) { - frames.push({n->GetChild(action), - frame.prob * static_cast(n->GetInfoset()->GetActionProb(action)), - frame.progress}); - } - continue; - } - - const auto &chain = chains.at(n->GetPlayer()); - const size_t index = frame.progress.at(n->GetPlayer()); - if (index + 1 >= chain.size()) { - // This player has already realised their designated sequence; any - // further move of theirs here is inconsistent with this profile. - continue; - } - const GameSequence &next = chain[index + 1]; - if (next->GetInfoset() != n->GetInfoset()) { - // This is not the information set at which this player's next - // designated move occurs; this branch cannot realise the profile. - continue; - } - auto progress = frame.progress; - progress[n->GetPlayer()] = index + 1; - frames.push({n->GetChild(next->GetAction()), frame.prob, std::move(progress)}); - } - return payoff; -} - } // end namespace Gambit diff --git a/src/games/gameseq.h b/src/games/gameseq.h index a4e6b95103..3190bcdfa7 100644 --- a/src/games/gameseq.h +++ b/src/games/gameseq.h @@ -47,13 +47,6 @@ class GameSequenceForm { const BehaviorSupportProfile &GetSupport() const { return m_support; } GameRep::Players GetPlayers() const { return m_support.GetGame()->GetPlayers(); } - - /// Returns the payoff to a player that arises when each player realises - /// the sequence assigned to them in the profile. This is computed on - /// demand by an (iterative, non-recursive) traversal of the game tree, - /// pruning as soon as a player's move is inconsistent with their - /// designated sequence. - Rational GetPayoff(const PureSequenceProfile &p_profile, const GamePlayer &p_player) const; }; } // end namespace Gambit diff --git a/src/games/seqpure.cc b/src/games/seqpure.cc index 5c7d1f5e4e..e981b4fe4d 100644 --- a/src/games/seqpure.cc +++ b/src/games/seqpure.cc @@ -20,10 +20,91 @@ // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. // +#include + #include "gambit.h" namespace Gambit { PureSequenceProfile::PureSequenceProfile(const Game &p_efg) : m_efg(p_efg) {} +namespace { + +/// One frame of the explicit-stack traversal used by +/// PureSequenceProfile::GetPayoff. Tracks the node under consideration, +/// the probability of reaching it (given chance's actual probabilities and +/// each player's moves as prescribed by the profile being evaluated), and +/// how far each player has progressed along the chain of sequences from +/// the empty sequence to the one designated for them. +struct SequenceWalkFrame { + GameNode node; + Rational prob; + std::map progress; +}; + +} // end anonymous namespace + +Rational PureSequenceProfile::GetPayoff(const GamePlayer &p_player) const +{ + std::map> chains; + std::map initialProgress; + for (auto player : m_efg->GetPlayers()) { + std::vector chain; + for (GameSequence seq = GetSequence(player); seq; seq = seq->GetParent()) { + chain.push_back(seq); + } + std::reverse(chain.begin(), chain.end()); // chain.front() is the empty sequence + chains[player] = chain; + initialProgress[player] = 0; + } + + Rational payoff(0); + std::stack frames; + frames.push({m_efg->GetRoot(), Rational(1), initialProgress}); + + while (!frames.empty()) { + const SequenceWalkFrame frame = std::move(frames.top()); + frames.pop(); + const GameNode &n = frame.node; + + if (n->GetOutcome()) { + const bool matches = std::all_of(chains.begin(), chains.end(), [&](const auto &entry) { + return frame.progress.at(entry.first) + 1 == entry.second.size(); + }); + if (matches) { + payoff += frame.prob * n->GetOutcome()->GetPayoff(p_player); + } + } + if (!n->GetInfoset()) { + continue; + } + if (n->GetPlayer()->IsChance()) { + for (auto action : n->GetInfoset()->GetActions()) { + frames.push({n->GetChild(action), + frame.prob * static_cast(n->GetInfoset()->GetActionProb(action)), + frame.progress}); + } + continue; + } + + const auto &chain = chains.at(n->GetPlayer()); + const size_t index = frame.progress.at(n->GetPlayer()); + if (index + 1 >= chain.size()) { + // This player has already realised their designated sequence; any + // further move of theirs here is inconsistent with this profile. + continue; + } + const GameSequence &next = chain[index + 1]; + if (next->GetInfoset() != n->GetInfoset()) { + // This is not the information set at which this player's next + // designated move occurs; this branch cannot realise the profile. + continue; + } + auto progress = frame.progress; + progress[n->GetPlayer()] = index + 1; + frames.push({n->GetChild(next->GetAction()), frame.prob, std::move(progress)}); + } + return payoff; +} + } // end namespace Gambit diff --git a/src/games/seqpure.h b/src/games/seqpure.h index 9fc6087079..289313d421 100644 --- a/src/games/seqpure.h +++ b/src/games/seqpure.h @@ -65,6 +65,13 @@ class PureSequenceProfile { m_profile[p_sequence->GetPlayer()] = p_sequence; } //@} + + /// Returns the payoff to a player that arises when each player realises + /// the sequence assigned to them in this profile. This is computed by + /// an (iterative, non-recursive) traversal of the game tree, pruning as + /// soon as a player's move is inconsistent with their designated + /// sequence. + Rational GetPayoff(const GamePlayer &p_player) const; }; } // end namespace Gambit diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 34d0af9832..07432f681d 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -111,7 +111,7 @@ Polynomial GetPayoff(ProblemData &p_data, const GamePlayer &p_player) Polynomial equation(p_data.space); for (auto profile : p_data.m_support.GetSequenceContingencies()) { - auto pay = p_data.m_support.GetPayoff(profile, p_player); + auto pay = profile.GetPayoff(p_player); if (pay != Rational(0)) { Polynomial term(p_data.space, double(pay)); for (auto player : p_data.m_support.GetPlayers()) { From 5c773a1137d5b322a622a1cca2bcf5b5f80089e1 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 22:20:21 -0400 Subject: [PATCH 5/6] Remove obsolete GameSequenceForm as fully refactored into other places. --- Makefile.am | 2 -- src/games/behavspt.cc | 64 ++++++++++++++++++--------------- src/games/behavspt.h | 34 +++++++++--------- src/games/gameseq.cc | 41 --------------------- src/games/gameseq.h | 54 ---------------------------- src/solvers/enumpoly/efgpoly.cc | 1 - 6 files changed, 54 insertions(+), 142 deletions(-) delete mode 100644 src/games/gameseq.cc delete mode 100644 src/games/gameseq.h diff --git a/Makefile.am b/Makefile.am index 835e0f0e97..c0dd7d45d7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -242,8 +242,6 @@ agg_SOURCES = \ game_SOURCES = \ src/gambit.h \ - src/games/gameseq.cc \ - src/games/gameseq.h \ src/games/ndarray.h \ src/games/number.h \ src/games/gameobject.h \ diff --git a/src/games/behavspt.cc b/src/games/behavspt.cc index 5279a3c09c..a838a7dbab 100644 --- a/src/games/behavspt.cc +++ b/src/games/behavspt.cc @@ -21,7 +21,6 @@ // #include "gambit.h" -#include "gameseq.h" namespace Gambit { @@ -66,6 +65,7 @@ size_t BehaviorSupportProfile::BehaviorProfileLength() const void BehaviorSupportProfile::AddAction(const GameAction &p_action) { m_reachableInfosets = nullptr; + m_sequences = nullptr; auto &support = m_actions.at(p_action->GetInfoset()); auto pos = std::find_if(support.begin(), support.end(), [p_action](const GameAction &a) { return a->GetNumber() >= p_action->GetNumber(); @@ -84,6 +84,7 @@ void BehaviorSupportProfile::AddAction(const GameAction &p_action) bool BehaviorSupportProfile::RemoveAction(const GameAction &p_action) { m_reachableInfosets = nullptr; + m_sequences = nullptr; auto &support = m_actions.at(p_action->GetInfoset()); auto pos = std::find(support.begin(), support.end(), p_action); if (pos != support.end()) { @@ -147,12 +148,19 @@ void BehaviorSupportProfile::DeactivateSubtree(const GameNode &n) // BehaviorSupportProfile: Sequence form //======================================================================== -std::shared_ptr BehaviorSupportProfile::GetSequenceForm() const +std::shared_ptr BehaviorSupportProfile::GetSequenceMap() const { - if (!m_sequenceForm) { - m_sequenceForm = std::make_shared(*this); + if (!m_sequences) { + m_sequences = std::make_shared(); + for (const auto &player : GetGame()->GetPlayers()) { + for (const auto &sequence : player->GetSequences()) { + if (!sequence->GetAction() || Contains(sequence->GetAction())) { + (*m_sequences)[player].emplace_back(sequence); + } + } + } } - return m_sequenceForm; + return m_sequences; } BehaviorSupportProfile::Sequences BehaviorSupportProfile::GetSequences() const { return {this}; } @@ -190,8 +198,8 @@ BehaviorSupportProfile::ToMixedBehaviorProfile(const std::mapGetSequenceForm()->m_sequences.cbegin(), - m_support->GetSequenceForm()->m_sequences.cend(), 0, + const auto sequences = m_support->GetSequenceMap(); + return std::accumulate(sequences->cbegin(), sequences->cend(), 0, [](int acc, const std::pair> &seq) { return acc + seq.second.size(); }); @@ -199,22 +207,22 @@ size_t BehaviorSupportProfile::Sequences::size() const BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::begin() const { - return {m_support->GetSequenceForm(), false}; + return {m_support->GetSequenceMap(), false}; } BehaviorSupportProfile::Sequences::iterator BehaviorSupportProfile::Sequences::end() const { - return {m_support->GetSequenceForm(), true}; + return {m_support->GetSequenceMap(), true}; } BehaviorSupportProfile::Sequences::iterator::iterator( - const std::shared_ptr p_sfg, bool p_end) - : m_sfg(p_sfg) + const std::shared_ptr p_sequences, bool p_end) + : m_sequences(p_sequences) { if (p_end) { - m_currentPlayer = m_sfg->m_sequences.cend(); + m_currentPlayer = m_sequences->cend(); } else { - m_currentPlayer = m_sfg->m_sequences.cbegin(); + m_currentPlayer = m_sequences->cbegin(); m_currentSequence = m_currentPlayer->second.cbegin(); } } @@ -222,7 +230,7 @@ BehaviorSupportProfile::Sequences::iterator::iterator( BehaviorSupportProfile::Sequences::iterator & BehaviorSupportProfile::Sequences::iterator::operator++() { - if (m_currentPlayer == m_sfg->m_sequences.cend()) { + if (m_currentPlayer == m_sequences->cend()) { return *this; } m_currentSequence++; @@ -230,7 +238,7 @@ BehaviorSupportProfile::Sequences::iterator::operator++() return *this; } m_currentPlayer++; - if (m_currentPlayer != m_sfg->m_sequences.cend()) { + if (m_currentPlayer != m_sequences->cend()) { m_currentSequence = m_currentPlayer->second.cbegin(); } return *this; @@ -238,10 +246,10 @@ BehaviorSupportProfile::Sequences::iterator::operator++() bool BehaviorSupportProfile::Sequences::iterator::operator==(const iterator &it) const { - if (m_sfg != it.m_sfg || m_currentPlayer != it.m_currentPlayer) { + if (m_sequences != it.m_sequences || m_currentPlayer != it.m_currentPlayer) { return false; } - if (m_currentPlayer == m_sfg->m_sequences.end()) { + if (m_currentPlayer == m_sequences->end()) { return true; } return (m_currentSequence == it.m_currentSequence); @@ -249,42 +257,42 @@ bool BehaviorSupportProfile::Sequences::iterator::operator==(const iterator &it) std::vector::const_iterator BehaviorSupportProfile::PlayerSequences::begin() const { - return m_support->GetSequenceForm()->m_sequences.at(m_player).begin(); + return m_support->GetSequenceMap()->at(m_player).begin(); } std::vector::const_iterator BehaviorSupportProfile::PlayerSequences::end() const { - return m_support->GetSequenceForm()->m_sequences.at(m_player).end(); + return m_support->GetSequenceMap()->at(m_player).end(); } size_t BehaviorSupportProfile::PlayerSequences::size() const { - return m_support->GetSequenceForm()->m_sequences.at(m_player).size(); + return m_support->GetSequenceMap()->at(m_player).size(); } BehaviorSupportProfile::SequenceContingencies::iterator::iterator( - const std::shared_ptr p_sfg, bool p_end) - : m_sfg(p_sfg), m_end(p_end) + const Game &p_efg, const std::shared_ptr p_sequences, bool p_end) + : m_efg(p_efg), m_sequences(p_sequences), m_end(p_end) { - for (auto [player, sequences] : m_sfg->m_sequences) { + for (auto [player, sequences] : *m_sequences) { m_indices[player] = 0; } } PureSequenceProfile BehaviorSupportProfile::SequenceContingencies::iterator::operator*() const { - PureSequenceProfile ret(m_sfg->GetSupport().GetGame()); + PureSequenceProfile ret(m_efg); for (auto [player, index] : m_indices) { - ret.SetSequence(m_sfg->m_sequences.at(player)[index]); + ret.SetSequence(m_sequences->at(player)[index]); } return ret; } PureSequenceProfile BehaviorSupportProfile::SequenceContingencies::iterator::operator->() const { - PureSequenceProfile ret(m_sfg->GetSupport().GetGame()); + PureSequenceProfile ret(m_efg); for (auto [player, index] : m_indices) { - ret.SetSequence(m_sfg->m_sequences.at(player)[index]); + ret.SetSequence(m_sequences->at(player)[index]); } return ret; } @@ -293,7 +301,7 @@ BehaviorSupportProfile::SequenceContingencies::iterator & BehaviorSupportProfile::SequenceContingencies::iterator::operator++() { for (auto [player, index] : m_indices) { - if (index < m_sfg->m_sequences.at(player).size() - 1) { + if (index < m_sequences->at(player).size() - 1) { m_indices[player]++; return *this; } diff --git a/src/games/behavspt.h b/src/games/behavspt.h index 4c5ae878ee..e14769ffc5 100644 --- a/src/games/behavspt.h +++ b/src/games/behavspt.h @@ -30,12 +30,6 @@ namespace Gambit { -class GameSequenceForm; -class SequencesWrapper; -class PlayerSequencesWrapper; -class InfosetsWrapper; -class ContingenciesWrapper; - /// This class represents a subset of the actions in an extensive game. /// It is enforced that each player has at least one action at each /// information set; thus, the actions in a support can be viewed as @@ -44,9 +38,15 @@ class ContingenciesWrapper; /// computational approaches that enumerate possible equilibrium /// supports. class BehaviorSupportProfile { +public: + /// The sequences of each player that are consistent with this support, + /// i.e. those whose action is in the support (or the empty sequence). + using SequenceMap = std::map>; + +private: Game m_efg; std::map> m_actions; - mutable std::shared_ptr m_sequenceForm; + mutable std::shared_ptr m_sequences; mutable std::shared_ptr> m_reachableInfosets; std::map m_infosetReachable; @@ -55,6 +55,7 @@ class BehaviorSupportProfile { bool HasReachableMembers(const GameInfoset &) const; void ActivateSubtree(const GameNode &); void DeactivateSubtree(const GameNode &); + std::shared_ptr GetSequenceMap() const; public: class Support { @@ -157,12 +158,12 @@ class BehaviorSupportProfile { public: class iterator { - const std::shared_ptr m_sfg; - std::map>::const_iterator m_currentPlayer; + const std::shared_ptr m_sequences; + SequenceMap::const_iterator m_currentPlayer; std::vector::const_iterator m_currentSequence; public: - iterator(const std::shared_ptr p_sfg, bool p_end); + iterator(const std::shared_ptr p_sequences, bool p_end); GameSequence operator*() const { return *m_currentSequence; } GameSequence operator->() const { return *m_currentSequence; } @@ -204,14 +205,16 @@ class BehaviorSupportProfile { class iterator { private: - const std::shared_ptr m_sfg; + Game m_efg; + const std::shared_ptr m_sequences; bool m_end{false}; std::map m_indices; public: using iterator_category = std::input_iterator_tag; - iterator(const std::shared_ptr p_sfg, bool p_end = false); + iterator(const Game &p_efg, const std::shared_ptr p_sequences, + bool p_end = false); PureSequenceProfile operator*() const; @@ -221,16 +224,15 @@ class BehaviorSupportProfile { bool operator==(const iterator &it) const { - return (m_end == it.m_end && m_sfg == it.m_sfg && m_indices == it.m_indices); + return (m_end == it.m_end && m_sequences == it.m_sequences && m_indices == it.m_indices); } bool operator!=(const iterator &it) const { return !(*this == it); } }; - iterator begin() { return {m_support->GetSequenceForm()}; } - iterator end() { return {m_support->GetSequenceForm(), true}; } + iterator begin() { return {m_support->GetGame(), m_support->GetSequenceMap()}; } + iterator end() { return {m_support->GetGame(), m_support->GetSequenceMap(), true}; } }; - std::shared_ptr GetSequenceForm() const; Sequences GetSequences() const; PlayerSequences GetSequences(const GamePlayer &p_player) const; GameRep::Players GetPlayers() const { return GetGame()->GetPlayers(); } diff --git a/src/games/gameseq.cc b/src/games/gameseq.cc deleted file mode 100644 index 24b8470910..0000000000 --- a/src/games/gameseq.cc +++ /dev/null @@ -1,41 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/solvers/enumpoly/sfg.cc -// Implementation of sequence form classes -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#include "gambit.h" -#include "gameseq.h" - -using namespace Gambit; - -namespace Gambit { - -void GameSequenceForm::BuildSequences() -{ - for (const auto &player : GetPlayers()) { - for (const auto &sequence : player->GetSequences()) { - if (!sequence->GetAction() || m_support.Contains(sequence->GetAction())) { - m_sequences[player].emplace_back(sequence); - } - } - } -} - -} // end namespace Gambit diff --git a/src/games/gameseq.h b/src/games/gameseq.h deleted file mode 100644 index 3190bcdfa7..0000000000 --- a/src/games/gameseq.h +++ /dev/null @@ -1,54 +0,0 @@ -// -// This file is part of Gambit -// Copyright (c) 1994-2026, The Gambit Project (https://www.gambit-project.org) -// -// FILE: src/solvers/enumpoly/gameseq.h -// Interface to sequence form classes -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 2 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program; if not, write to the Free Software -// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -// - -#ifndef GAMESEQ_H -#define GAMESEQ_H - -#include "gambit.h" -#include "seqpure.h" - -namespace Gambit { - -class GameSequenceForm { - friend class BehaviorSupportProfile; - - BehaviorSupportProfile m_support; - std::map> m_sequences; - - void BuildSequences(); - -public: - explicit GameSequenceForm(const BehaviorSupportProfile &p_support) : m_support(p_support) - { - BuildSequences(); - } - - ~GameSequenceForm() = default; - - const BehaviorSupportProfile &GetSupport() const { return m_support; } - - GameRep::Players GetPlayers() const { return m_support.GetGame()->GetPlayers(); } -}; - -} // end namespace Gambit - -#endif // GAMESEQ_H diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 07432f681d..8e5b05d6d4 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -22,7 +22,6 @@ #include "enumpoly.h" #include "solvers/nashsupport/nashsupport.h" -#include "games/gameseq.h" #include "polysystem.h" #include "polysolver.h" #include "indexproduct.h" From 258c7777e466cc12ad8e4029cc195ef17394c789 Mon Sep 17 00:00:00 2001 From: Theodore Turocy Date: Thu, 30 Jul 2026 22:32:57 -0400 Subject: [PATCH 6/6] Clean up hangovers from refactorings. --- src/solvers/enumpoly/efgpoly.cc | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/solvers/enumpoly/efgpoly.cc b/src/solvers/enumpoly/efgpoly.cc index 8e5b05d6d4..403754c68d 100644 --- a/src/solvers/enumpoly/efgpoly.cc +++ b/src/solvers/enumpoly/efgpoly.cc @@ -63,24 +63,31 @@ class ProblemData { explicit ProblemData(const BehaviorSupportProfile &p_support); }; -Polynomial BuildSequenceVariable(ProblemData &p_data, const GameSequence &p_sequence, - const std::map &var) +// A sequence's variable is eliminated in favour of the other sequences at +// its information set exactly when it is the last (in the support's +// ordering) action at that information set. +bool IsEliminated(const BehaviorSupportProfile &p_support, const GameSequence &p_sequence) +{ + return p_sequence->GetAction() == p_support.GetActions(p_sequence->GetInfoset()).back(); +} + +Polynomial BuildSequenceVariable(ProblemData &p_data, const GameSequence &p_sequence) { if (!p_sequence->GetAction()) { return Polynomial(p_data.space, 1); } - if (p_sequence->GetAction() != p_data.m_support.GetActions(p_sequence->GetInfoset()).back()) { - return Polynomial(p_data.space, var.at(p_sequence), 1); + if (!IsEliminated(p_data.m_support, p_sequence)) { + return Polynomial(p_data.space, p_data.var.at(p_sequence), 1); } // The last action at an information set is eliminated using the // sum-to-one relation among the sequences at that information set: its // probability is that of its parent sequence, less the probabilities of // its sibling sequences (the other actions at the same information set). - Polynomial equation = BuildSequenceVariable(p_data, p_sequence->GetParent(), var); + Polynomial equation = BuildSequenceVariable(p_data, p_sequence->GetParent()); for (const auto &sibling : p_data.siblings.at(p_sequence->GetInfoset())) { if (sibling != p_sequence) { - equation -= BuildSequenceVariable(p_data, sibling, var); + equation -= BuildSequenceVariable(p_data, sibling); } } return equation; @@ -94,14 +101,14 @@ ProblemData::ProblemData(const BehaviorSupportProfile &p_support) for (auto sequence : m_support.GetSequences()) { if (sequence->GetAction()) { siblings[sequence->GetInfoset()].push_back(sequence); - if (sequence->GetAction() != p_support.GetActions(sequence->GetInfoset()).back()) { + if (!IsEliminated(m_support, sequence)) { var[sequence] = var.size() + 1; } } } for (auto sequence : m_support.GetSequences()) { - variables.emplace(sequence, BuildSequenceVariable(*this, sequence, var)); + variables.emplace(sequence, BuildSequenceVariable(*this, sequence)); } } @@ -112,7 +119,7 @@ Polynomial GetPayoff(ProblemData &p_data, const GamePlayer &p_player) for (auto profile : p_data.m_support.GetSequenceContingencies()) { auto pay = profile.GetPayoff(p_player); if (pay != Rational(0)) { - Polynomial term(p_data.space, double(pay)); + Polynomial term(p_data.space, pay); for (auto player : p_data.m_support.GetPlayers()) { term *= p_data.variables.at(profile.GetSequence(player)); } @@ -127,13 +134,11 @@ void IndifferenceEquations(ProblemData &p_data, PolynomialSystem &p_equa for (auto player : p_data.m_support.GetPlayers()) { const Polynomial payoff = GetPayoff(p_data, player); for (auto sequence : p_data.m_support.GetSequences(player)) { - try { - p_equations.push_back(payoff.PartialDerivative(p_data.var.at(sequence))); - } - catch (std::out_of_range &) { - // This sequence's variable was already substituted out in terms of - // the probabilities of other sequences + if (auto it = p_data.var.find(sequence); it != p_data.var.end()) { + p_equations.push_back(payoff.PartialDerivative(it->second)); } + // Sequences with no entry in p_data.var have had their variable + // substituted out in terms of the probabilities of other sequences. } } } @@ -144,8 +149,8 @@ void LastActionProbPositiveInequalities(ProblemData &p_data, PolynomialSystemGetAction()) { continue; } - const auto &actions = p_data.m_support.GetActions(sequence->GetAction()->GetInfoset()); - if (actions.size() > 1 && sequence->GetAction() == actions.back()) { + if (p_data.m_support.GetActions(sequence->GetInfoset()).size() > 1 && + IsEliminated(p_data.m_support, sequence)) { p_equations.push_back(p_data.variables.at(sequence)); } }