Skip to content

Commit 4d906c7

Browse files
Merge branch 'fix/659' into add-drawtree-to-tutorials
2 parents 2f8227b + 3504b77 commit 4d906c7

31 files changed

Lines changed: 456 additions & 565 deletions

.github/workflows/python.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
1111
strategy:
1212
matrix:
13-
python-version: ['3.10', '3.13']
13+
python-version: ['3.10', '3.14']
1414

1515
steps:
1616
- uses: actions/checkout@v5
@@ -39,7 +39,7 @@ jobs:
3939
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
4040
strategy:
4141
matrix:
42-
python-version: ['3.13']
42+
python-version: ['3.14']
4343

4444
steps:
4545
- uses: actions/checkout@v4
@@ -52,7 +52,6 @@ jobs:
5252
python -m pip install --upgrade pip
5353
pip install setuptools build cython wheel
5454
pip install -r tests/requirements.txt
55-
pip install -r doc/requirements.txt
5655
- name: Build extension
5756
run: |
5857
python -m pip install -v .
@@ -64,7 +63,7 @@ jobs:
6463
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
6564
strategy:
6665
matrix:
67-
python-version: ['3.13']
66+
python-version: ['3.14']
6867

6968
steps:
7069
- uses: actions/checkout@v4
@@ -89,7 +88,7 @@ jobs:
8988
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
9089
strategy:
9190
matrix:
92-
python-version: ['3.13']
91+
python-version: ['3.14']
9392

9493
steps:
9594
- uses: actions/checkout@v5

ChangeLog

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212

1313
### Added
1414
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
15+
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
16+
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
17+
taken by the player before reaching the node or information set, respectively. (#582)
1518
- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine
1619
if the node is reachable by at least one pure strategy profile. This proves useful for identifying
1720
unreachable parts of the game tree in games with absent-mindedness. (#629)

doc/developer.contributing.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ You can also run the tests locally before submitting your pull request, using `p
9999

100100
pytest
101101

102+
3. [Optional] If you wish to run the tutorial notebook tests, you will need to add the ``--run-tutorials`` flag, which require the `doc` dependencies: ::
103+
104+
pip install -r doc/requirements.txt
105+
pytest --run-tutorials
106+
102107
Adding to the test suite
103108
^^^^^^^^^^^^^^^^^^^^^^^^
104109

doc/pygambit.api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ Information about the game
149149
Node.player
150150
Node.is_successor_of
151151
Node.plays
152+
Node.own_prior_action
152153

153154
.. autosummary::
154155

@@ -162,6 +163,7 @@ Information about the game
162163
Infoset.members
163164
Infoset.precedes
164165
Infoset.plays
166+
Infoset.own_prior_actions
165167

166168
.. autosummary::
167169

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "pygambit"
77
version = "16.4.0"
88
description = "The package for computation in game theory"
99
readme = "src/README.rst"
10-
requires-python = ">=3.9"
10+
requires-python = ">=3.10"
1111
license = "GPL-2.0-or-later"
1212
authors = [
1313
{name = "Theodore Turocy", email = "ted.turocy@gmail.com"},
@@ -17,11 +17,11 @@ keywords = ["game theory", "Nash equilibrium"]
1717
classifiers=[
1818
"Development Status :: 5 - Production/Stable",
1919
"Intended Audience :: Science/Research",
20-
"Programming Language :: Python :: 3.9",
2120
"Programming Language :: Python :: 3.10",
2221
"Programming Language :: Python :: 3.11",
2322
"Programming Language :: Python :: 3.12",
2423
"Programming Language :: Python :: 3.13",
24+
"Programming Language :: Python :: 3.14",
2525
"Programming Language :: Python :: Implementation :: CPython",
2626
"Topic :: Scientific/Engineering :: Mathematics"
2727
]

src/games/behavpure.cc

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -98,30 +98,16 @@ MixedBehaviorProfile<Rational> PureBehaviorProfile::ToMixedBehaviorProfile() con
9898
// class BehaviorContingencies
9999
//========================================================================
100100

101-
BehaviorContingencies::BehaviorContingencies(const BehaviorSupportProfile &p_support,
102-
const std::set<GameInfoset> &p_reachable,
103-
const std::vector<GameAction> &p_frozen)
104-
: m_support(p_support), m_frozen(p_frozen)
101+
BehaviorContingencies::BehaviorContingencies(const BehaviorSupportProfile &p_support)
102+
: m_support(p_support)
105103
{
106-
if (!p_reachable.empty()) {
107-
for (const auto &infoset : p_reachable) {
108-
m_activeInfosets.push_back(infoset);
109-
}
110-
}
111-
else {
112-
for (const auto &player : m_support.GetGame()->GetPlayers()) {
113-
for (const auto &infoset : player->GetInfosets()) {
114-
if (p_support.IsReachable(infoset)) {
115-
m_activeInfosets.push_back(infoset);
116-
}
104+
for (const auto &player : m_support.GetGame()->GetPlayers()) {
105+
for (const auto &infoset : player->GetInfosets()) {
106+
if (p_support.IsReachable(infoset)) {
107+
m_reachableInfosets.push_back(infoset);
117108
}
118109
}
119110
}
120-
for (const auto &action : m_frozen) {
121-
m_activeInfosets.erase(std::find_if(
122-
m_activeInfosets.begin(), m_activeInfosets.end(),
123-
[action](const GameInfoset &infoset) { return infoset == action->GetInfoset(); }));
124-
}
125111
}
126112

127113
BehaviorContingencies::iterator::iterator(BehaviorContingencies *p_cont, bool p_end)
@@ -136,15 +122,12 @@ BehaviorContingencies::iterator::iterator(BehaviorContingencies *p_cont, bool p_
136122
m_profile.SetAction(*m_currentBehav[infoset]);
137123
}
138124
}
139-
for (const auto &action : m_cont->m_frozen) {
140-
m_profile.SetAction(action);
141-
}
142125
}
143126

144127
BehaviorContingencies::iterator &BehaviorContingencies::iterator::operator++()
145128
{
146-
for (auto infoset = m_cont->m_activeInfosets.crbegin();
147-
infoset != m_cont->m_activeInfosets.crend(); ++infoset) {
129+
for (auto infoset = m_cont->m_reachableInfosets.crbegin();
130+
infoset != m_cont->m_reachableInfosets.crend(); ++infoset) {
148131
++m_currentBehav[*infoset];
149132
if (m_currentBehav.at(*infoset) != m_cont->m_support.GetActions(*infoset).end()) {
150133
m_profile.SetAction(*m_currentBehav[*infoset]);

src/games/behavpure.h

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ namespace Gambit {
3232
/// It specifies exactly one strategy for each information set in the
3333
/// game.
3434
class PureBehaviorProfile {
35-
private:
3635
Game m_efg;
3736
std::map<GameInfoset, GameAction> m_profile;
3837

@@ -86,14 +85,11 @@ template <> inline std::string PureBehaviorProfile::GetPayoff(const GamePlayer &
8685
}
8786

8887
class BehaviorContingencies {
89-
private:
9088
BehaviorSupportProfile m_support;
91-
std::vector<GameAction> m_frozen;
92-
std::list<GameInfoset> m_activeInfosets;
89+
std::list<GameInfoset> m_reachableInfosets;
9390

9491
public:
9592
class iterator {
96-
private:
9793
BehaviorContingencies *m_cont;
9894
bool m_atEnd;
9995
std::map<GameInfoset, BehaviorSupportProfile::Support::const_iterator> m_currentBehav;
@@ -127,10 +123,8 @@ class BehaviorContingencies {
127123
};
128124
/// @name Lifecycle
129125
//@{
130-
/// Construct a new iterator on the support, holding the listed actions fixed
131-
explicit BehaviorContingencies(const BehaviorSupportProfile &,
132-
const std::set<GameInfoset> &p_active = {},
133-
const std::vector<GameAction> &p_frozen = {});
126+
/// Construct a new iterator on the support
127+
explicit BehaviorContingencies(const BehaviorSupportProfile &);
134128
//@}
135129
iterator begin() { return {this, false}; }
136130
iterator end() { return {this, true}; }

src/games/behavspt.cc

Lines changed: 0 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -106,106 +106,6 @@ std::list<GameInfoset> BehaviorSupportProfile::GetInfosets(const GamePlayer &p_p
106106
return answer;
107107
}
108108

109-
namespace {
110-
111-
void ReachableInfosets(const BehaviorSupportProfile &p_support, const GameNode &p_node,
112-
std::set<GameInfoset> &p_reached)
113-
{
114-
if (p_node->IsTerminal()) {
115-
return;
116-
}
117-
118-
const GameInfoset infoset = p_node->GetInfoset();
119-
if (!infoset->GetPlayer()->IsChance()) {
120-
p_reached.insert(infoset);
121-
for (const auto &action : p_support.GetActions(infoset)) {
122-
ReachableInfosets(p_support, p_node->GetChild(action), p_reached);
123-
}
124-
}
125-
else {
126-
for (const auto &child : p_node->GetChildren()) {
127-
ReachableInfosets(p_support, child, p_reached);
128-
}
129-
}
130-
}
131-
132-
} // end anonymous namespace
133-
134-
bool BehaviorSupportProfile::Dominates(const GameAction &a, const GameAction &b,
135-
bool p_strict) const
136-
{
137-
const GameInfoset infoset = a->GetInfoset();
138-
if (infoset != b->GetInfoset()) {
139-
throw UndefinedException();
140-
}
141-
142-
GamePlayer player = infoset->GetPlayer();
143-
int thesign = 0;
144-
145-
auto nodelist = GetMembers(infoset);
146-
for (const auto &node : GetMembers(infoset)) {
147-
std::set<GameInfoset> reachable;
148-
ReachableInfosets(*this, node->GetChild(a), reachable);
149-
ReachableInfosets(*this, node->GetChild(b), reachable);
150-
151-
auto contingencies = BehaviorContingencies(*this, reachable);
152-
if (p_strict) {
153-
if (!std::all_of(contingencies.begin(), contingencies.end(),
154-
[&](const PureBehaviorProfile &profile) {
155-
return profile.GetPayoff<Rational>(node->GetChild(a), player) >
156-
profile.GetPayoff<Rational>(node->GetChild(b), player);
157-
})) {
158-
return false;
159-
}
160-
}
161-
else {
162-
for (const auto &iter : contingencies) {
163-
auto newsign = sign(iter.GetPayoff<Rational>(node->GetChild(a), player) -
164-
iter.GetPayoff<Rational>(node->GetChild(b), player));
165-
if (newsign < 0) {
166-
return false;
167-
}
168-
thesign = std::max(thesign, newsign);
169-
}
170-
}
171-
}
172-
return p_strict || thesign > 0;
173-
}
174-
175-
bool BehaviorSupportProfile::IsDominated(const GameAction &p_action, const bool p_strict) const
176-
{
177-
const auto &actions = GetActions(p_action->GetInfoset());
178-
return std::any_of(actions.begin(), actions.end(), [&](const GameAction &a) {
179-
return a != p_action && Dominates(a, p_action, p_strict);
180-
});
181-
}
182-
183-
BehaviorSupportProfile BehaviorSupportProfile::Undominated(const bool p_strict) const
184-
{
185-
BehaviorSupportProfile result(*this);
186-
for (const auto &player : m_efg->GetPlayers()) {
187-
for (const auto &infoset : player->GetInfosets()) {
188-
const auto &actions = GetActions(infoset);
189-
std::set<GameAction> dominated;
190-
for (const auto &action1 : actions) {
191-
if (contains(dominated, action1)) {
192-
continue;
193-
}
194-
for (const auto &action2 : actions) {
195-
if (action1 == action2 || contains(dominated, action2)) {
196-
continue;
197-
}
198-
if (Dominates(action1, action2, p_strict)) {
199-
dominated.insert(action2);
200-
result.RemoveAction(action2);
201-
}
202-
}
203-
}
204-
}
205-
}
206-
return result;
207-
}
208-
209109
bool BehaviorSupportProfile::HasReachableMembers(const GameInfoset &p_infoset) const
210110
{
211111
const auto &members = p_infoset->GetMembers();

src/games/game.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ class GameInfosetRep : public std::enable_shared_from_this<GameInfosetRep> {
247247

248248
bool Precedes(GameNode) const;
249249

250+
std::set<GameAction> GetOwnPriorActions() const;
251+
250252
const Number &GetActionProb(const GameAction &p_action) const
251253
{
252254
if (p_action->GetInfoset().get() != this) {
@@ -492,6 +494,7 @@ class GameNodeRep : public std::enable_shared_from_this<GameNodeRep> {
492494
bool IsTerminal() const { return m_children.empty(); }
493495
GamePlayer GetPlayer() const { return (m_infoset) ? m_infoset->GetPlayer() : nullptr; }
494496
GameAction GetPriorAction() const; // returns null if root node
497+
GameAction GetOwnPriorAction() const;
495498
GameNode GetParent() const { return (m_parent) ? m_parent->shared_from_this() : nullptr; }
496499
GameNode GetNextSibling() const;
497500
GameNode GetPriorSibling() const;
@@ -899,6 +902,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
899902
virtual std::vector<GameInfoset> GetInfosets() const { throw UndefinedException(); }
900903
/// Sort the information sets for each player in a canonical order
901904
virtual void SortInfosets() {}
905+
/// Returns the set of actions taken by the infoset's owner before reaching this infoset
906+
virtual std::set<GameAction> GetOwnPriorActions(const GameInfoset &p_infoset) const
907+
{
908+
throw UndefinedException();
909+
}
902910
//@}
903911

904912
/// @name Outcomes
@@ -926,6 +934,11 @@ class GameRep : public std::enable_shared_from_this<GameRep> {
926934
virtual size_t NumNodes() const = 0;
927935
/// Returns the number of non-terminal nodes in the game
928936
virtual size_t NumNonterminalNodes() const = 0;
937+
/// Returns the last action taken by the node's owner before reaching this node
938+
virtual GameAction GetOwnPriorAction(const GameNode &p_node) const
939+
{
940+
throw UndefinedException();
941+
}
929942
//@}
930943

931944
/// @name Modification

0 commit comments

Comments
 (0)