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
19 changes: 10 additions & 9 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

## [16.5.0] - unreleased

### Fixed
- Sequence-form based equilibrium-finding methods returned incorrect output on games with
outcomes at non-terminal nodes. (#654)
### Added
- Implement `IsAbsentMinded()` on information sets (C++) and `Infoset.is_absent_minded` (Python)
to detect if an information is absent-minded.
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
taken by the player before reaching the node or information set, respectively. (#582)
- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine
if the node is reachable by at least one pure strategy profile. This proves useful for identifying
unreachable parts of the game tree in games with absent-mindedness. (#629)

### Changed
- Labels for players, outcomes, strategies, and actions are expected to be non-empty and unique within
Expand All @@ -27,15 +37,6 @@
Iteration ordering of information sets and their members is ensured internally. `sort_infosets`
is therefore now a no-op and is deprecated; it will be removed in a future version.

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

### Removed
- Eliminating dominated actions has been removed from the GUI as it was implementing a non-standard
formulation of dominance. (#612)
Expand Down
21 changes: 11 additions & 10 deletions src/solvers/lcp/efglcp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ template <class T> class NashLcpBehaviorSolver {

class Solution;

void FillTableau(Matrix<T> &, const GameNode &, T, int, int, Solution &) const;
void FillTableau(Matrix<T> &, const GameNode &, T, int, int, T, T, Solution &) const;
void AllLemke(const Game &, int dup, linalg::LemkeTableau<T> &B, int depth, Matrix<T> &,
Solution &) const;
void GetProfile(const linalg::LemkeTableau<T> &tab, MixedBehaviorProfile<T> &, const Vector<T> &,
Expand Down Expand Up @@ -126,7 +126,8 @@ std::list<MixedBehaviorProfile<T>> NashLcpBehaviorSolver<T>::Solve(const Game &p
const int ntot = solution.ns1 + solution.ns2 + solution.ni1 + solution.ni2;
Matrix<T> A(1, ntot, 0, ntot);
A = static_cast<T>(0);
FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1, solution);
FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1, static_cast<T>(0), static_cast<T>(0),
solution);
for (int i = A.MinRow(); i <= A.MaxRow(); i++) {
A(i, 0) = static_cast<T>(-1);
}
Expand Down Expand Up @@ -238,28 +239,28 @@ void NashLcpBehaviorSolver<T>::AllLemke(const Game &p_game, int j, linalg::Lemke

template <class T>
void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T prob, int s1, int s2,
Solution &p_solution) const
T payoff1, T payoff2, Solution &p_solution) const
{
const int ns1 = p_solution.ns1;
const int ns2 = p_solution.ns2;
const int ni1 = p_solution.ni1;

const GameOutcome outcome = n->GetOutcome();
if (outcome) {
A(s1, ns1 + s2) += Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1)) -
p_solution.maxpay);
A(ns1 + s2, s1) += Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(2)) -
p_solution.maxpay);
payoff1 += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1));
payoff2 += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(2));
}
if (n->IsTerminal()) {
A(s1, ns1 + s2) += Rational(prob) * (payoff1 - p_solution.maxpay);
A(ns1 + s2, s1) += Rational(prob) * (payoff2 - p_solution.maxpay);
return;
}
const GameInfoset infoset = n->GetInfoset();
if (n->GetPlayer()->IsChance()) {
for (const auto &action : infoset->GetActions()) {
FillTableau(A, n->GetChild(action),
Rational(prob) * static_cast<Rational>(infoset->GetActionProb(action)), s1, s2,
p_solution);
payoff1, payoff2, p_solution);
}
}
else if (n->GetPlayer()->GetNumber() == 1) {
Expand All @@ -271,7 +272,7 @@ void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T pr
snew++;
A(snew, infoset_idx) = static_cast<T>(1);
A(infoset_idx, snew) = static_cast<T>(-1);
FillTableau(A, child, prob, snew, s2, p_solution);
FillTableau(A, child, prob, snew, s2, payoff1, payoff2, p_solution);
}
}
else {
Expand All @@ -283,7 +284,7 @@ void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T pr
snew++;
A(ns1 + snew, infoset_idx) = static_cast<T>(1);
A(infoset_idx, ns1 + snew) = static_cast<T>(-1);
FillTableau(A, child, prob, s1, snew, p_solution);
FillTableau(A, child, prob, s1, snew, payoff1, payoff2, p_solution);
}
}
}
Expand Down
17 changes: 9 additions & 8 deletions src/solvers/lp/lp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ template <class T> class GameData {

explicit GameData(const Game &);

void FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2);
void FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2, T payoff);

void GetBehavior(MixedBehaviorProfile<T> &v, const Array<T> &, const Array<T> &,
const GameNode &, int, int);
Expand All @@ -57,21 +57,22 @@ template <class T> GameData<T>::GameData(const Game &p_game) : minpay(p_game->Ge
// Recursively fills the constraint matrix A for the subtree rooted at 'n'.
//
template <class T>
void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2)
void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2,
T payoff)
{
const GameOutcome outcome = n->GetOutcome();
if (outcome) {
A(s1, s2) +=
Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1)) - minpay);
payoff += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1));
}
if (n->IsTerminal()) {
A(s1, s2) += Rational(prob) * (payoff - minpay);
return;
}
const GameInfoset infoset = n->GetInfoset();
if (n->GetPlayer()->IsChance()) {
for (const auto &action : infoset->GetActions()) {
FillTableau(A, n->GetChild(action), prob * static_cast<T>(infoset->GetActionProb(action)),
s1, s2);
s1, s2, payoff);
}
}
else if (n->GetPlayer()->GetNumber() == 1) {
Expand All @@ -80,7 +81,7 @@ void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, in
A(s1, col) = static_cast<T>(1);
for (const auto &child : n->GetChildren()) {
A(++snew, col) = static_cast<T>(-1);
FillTableau(A, child, prob, snew, s2);
FillTableau(A, child, prob, snew, s2, payoff);
}
}
else {
Expand All @@ -89,7 +90,7 @@ void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, in
A(row, s2) = static_cast<T>(-1);
for (const auto &child : n->GetChildren()) {
A(row, ++snew) = static_cast<T>(1);
FillTableau(A, child, prob, s1, snew);
FillTableau(A, child, prob, s1, snew, payoff);
}
}
}
Expand Down Expand Up @@ -187,7 +188,7 @@ std::list<MixedBehaviorProfile<T>> LpBehaviorSolve(const Game &p_game,
b = static_cast<T>(0);
c = static_cast<T>(0);

data.FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1);
data.FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1, static_cast<T>(0));
A(1, data.ns2 + 1) = static_cast<T>(-1);
A(data.ns1 + 1, 1) = static_cast<T>(1);

Expand Down
45 changes: 15 additions & 30 deletions tests/test_nash.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,23 +466,20 @@ def test_lcp_behavior_double():
games.create_2x2_zero_sum_efg(),
[[["1/2", "1/2"]], [["1/2", "1/2"]]]
),
pytest.param(
(
games.create_2x2_zero_sum_efg(missing_term_outcome=True),
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(games.create_matching_pennies_efg(),
[[["1/2", "1/2"]], [["1/2", "1/2"]]]),
pytest.param(
(
games.create_matching_pennies_efg(with_neutral_outcome=True),
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(games.create_stripped_down_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]),
pytest.param(
(
games.create_stripped_down_poker_efg(nonterm_outcomes=True),
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_kuhn_poker_efg(),
Expand All @@ -498,7 +495,7 @@ def test_lcp_behavior_double():
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
],
),
pytest.param(
(
games.create_kuhn_poker_efg(nonterm_outcomes=True),
[
[
Expand All @@ -511,7 +508,6 @@ def test_lcp_behavior_double():
],
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
# In the next test case:
# 1/2-1/2 for l/r is determined by MixedBehaviorProfile.UndefinedToCentroid()
Expand All @@ -534,13 +530,12 @@ def test_lcp_behavior_double():
[["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]],
]
),
pytest.param(
(
games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True),
[
[["1/3", 0, "2/3"], ["2/3", 0, "1/3"]],
[["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]],
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_large_payoff_game_efg(),
Expand All @@ -557,13 +552,12 @@ def test_lcp_behavior_double():
[[1, 0], ["6/11", "5/11"]]
]
),
pytest.param(
(
games.create_chance_in_middle_efg(nonterm_outcomes=True),
[
[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]],
[[1, 0], ["6/11", "5/11"]]
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
# Non-zero-sum games
(
Expand All @@ -586,19 +580,17 @@ def test_lcp_behavior_double():
games.create_entry_accomodation_efg(),
[[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]]
),
pytest.param(
(
games.create_entry_accomodation_efg(nonterm_outcomes=True),
[[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_non_zero_sum_lacking_outcome_efg(),
[[["1/3", "2/3"]], [["1/2", "1/2"]]]
),
pytest.param(
(
games.create_non_zero_sum_lacking_outcome_efg(missing_term_outcome=True),
[[["1/3", "2/3"]], [["1/2", "1/2"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
],
)
Expand Down Expand Up @@ -677,35 +669,31 @@ def test_lp_behavior_double():
games.create_two_player_perfect_info_win_lose_efg(),
[[[0, 1], [1, 0]], [[1, 0], [1, 0]]],
),
pytest.param(
(
games.create_two_player_perfect_info_win_lose_efg(nonterm_outcomes=True),
[[[0, 1], [1, 0]], [[1, 0], [1, 0]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_2x2_zero_sum_efg(missing_term_outcome=False),
[[["1/2", "1/2"]], [["1/2", "1/2"]]]
),
pytest.param(
(
games.create_2x2_zero_sum_efg(missing_term_outcome=True),
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(games.create_matching_pennies_efg(with_neutral_outcome=False),
[[["1/2", "1/2"]], [["1/2", "1/2"]]]),
pytest.param(
(
games.create_matching_pennies_efg(with_neutral_outcome=True),
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_stripped_down_poker_efg(),
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
),
pytest.param(
(
games.create_stripped_down_poker_efg(nonterm_outcomes=True),
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_kuhn_poker_efg(),
Expand All @@ -714,7 +702,7 @@ def test_lp_behavior_double():
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
],
),
pytest.param(
(
games.create_kuhn_poker_efg(nonterm_outcomes=True),
[
[
Expand All @@ -727,7 +715,6 @@ def test_lp_behavior_double():
],
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_seq_form_STOC_paper_zero_sum_2_player_efg(),
Expand All @@ -747,13 +734,12 @@ def test_lp_behavior_double():
[["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]],
]
),
pytest.param(
(
games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True),
[
[["1/3", 0, "2/3"], ["2/3", 0, "1/3"]],
[["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]],
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
(
games.create_large_payoff_game_efg(),
Expand All @@ -770,13 +756,12 @@ def test_lp_behavior_double():
[[1, 0], ["6/11", "5/11"]]
],
),
pytest.param(
(
games.create_chance_in_middle_efg(nonterm_outcomes=True),
[
[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]],
[[1, 0], ["6/11", "5/11"]]
],
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
),
],
)
Expand Down