Skip to content

Commit 867d2b8

Browse files
Correct LP and LCP handling of payoffs at nonterminal nodes (#713)
The LP and LCP solvers for the sequence form did not correctly handle the case in which an extensive game has outcomes at nonterminal nodes - even if those outcomes have zero payoffs. This corrects the problem by pushing all payoffs down to the "terminal" sequence profiles when constructing the sequence form tensor. --------- Co-authored-by: Theodore Turocy <ted.turocy@gmail.com>
1 parent 55c3ecd commit 867d2b8

4 files changed

Lines changed: 45 additions & 57 deletions

File tree

ChangeLog

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22

33
## [16.5.0] - unreleased
44

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

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

30-
### Added
31-
- Tests for EFG Nash solvers -- `enumpoly_solve`, `lp_solve`, `lcp_solve` -- in behavior stratgegies
32-
- In `pygambit`, `Node` objects now have a read-only property `own_prior_action` and `Infoset` objects
33-
have a read-only property `own_prior_actions` to retrieve the last action or the set of last actions
34-
taken by the player before reaching the node or information set, respectively. (#582)
35-
- In `pygambit`, `Node` objects now have a read-only property `is_strategy_reachable` to determine
36-
if the node is reachable by at least one pure strategy profile. This proves useful for identifying
37-
unreachable parts of the game tree in games with absent-mindedness. (#629)
38-
3940
### Removed
4041
- Eliminating dominated actions has been removed from the GUI as it was implementing a non-standard
4142
formulation of dominance. (#612)

src/solvers/lcp/efglcp.cc

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ template <class T> class NashLcpBehaviorSolver {
4444

4545
class Solution;
4646

47-
void FillTableau(Matrix<T> &, const GameNode &, T, int, int, Solution &) const;
47+
void FillTableau(Matrix<T> &, const GameNode &, T, int, int, T, T, Solution &) const;
4848
void AllLemke(const Game &, int dup, linalg::LemkeTableau<T> &B, int depth, Matrix<T> &,
4949
Solution &) const;
5050
void GetProfile(const linalg::LemkeTableau<T> &tab, MixedBehaviorProfile<T> &, const Vector<T> &,
@@ -126,7 +126,8 @@ std::list<MixedBehaviorProfile<T>> NashLcpBehaviorSolver<T>::Solve(const Game &p
126126
const int ntot = solution.ns1 + solution.ns2 + solution.ni1 + solution.ni2;
127127
Matrix<T> A(1, ntot, 0, ntot);
128128
A = static_cast<T>(0);
129-
FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1, solution);
129+
FillTableau(A, p_game->GetRoot(), static_cast<T>(1), 1, 1, static_cast<T>(0), static_cast<T>(0),
130+
solution);
130131
for (int i = A.MinRow(); i <= A.MaxRow(); i++) {
131132
A(i, 0) = static_cast<T>(-1);
132133
}
@@ -238,28 +239,28 @@ void NashLcpBehaviorSolver<T>::AllLemke(const Game &p_game, int j, linalg::Lemke
238239

239240
template <class T>
240241
void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T prob, int s1, int s2,
241-
Solution &p_solution) const
242+
T payoff1, T payoff2, Solution &p_solution) const
242243
{
243244
const int ns1 = p_solution.ns1;
244245
const int ns2 = p_solution.ns2;
245246
const int ni1 = p_solution.ni1;
246247

247248
const GameOutcome outcome = n->GetOutcome();
248249
if (outcome) {
249-
A(s1, ns1 + s2) += Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1)) -
250-
p_solution.maxpay);
251-
A(ns1 + s2, s1) += Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(2)) -
252-
p_solution.maxpay);
250+
payoff1 += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1));
251+
payoff2 += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(2));
253252
}
254253
if (n->IsTerminal()) {
254+
A(s1, ns1 + s2) += Rational(prob) * (payoff1 - p_solution.maxpay);
255+
A(ns1 + s2, s1) += Rational(prob) * (payoff2 - p_solution.maxpay);
255256
return;
256257
}
257258
const GameInfoset infoset = n->GetInfoset();
258259
if (n->GetPlayer()->IsChance()) {
259260
for (const auto &action : infoset->GetActions()) {
260261
FillTableau(A, n->GetChild(action),
261262
Rational(prob) * static_cast<Rational>(infoset->GetActionProb(action)), s1, s2,
262-
p_solution);
263+
payoff1, payoff2, p_solution);
263264
}
264265
}
265266
else if (n->GetPlayer()->GetNumber() == 1) {
@@ -271,7 +272,7 @@ void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T pr
271272
snew++;
272273
A(snew, infoset_idx) = static_cast<T>(1);
273274
A(infoset_idx, snew) = static_cast<T>(-1);
274-
FillTableau(A, child, prob, snew, s2, p_solution);
275+
FillTableau(A, child, prob, snew, s2, payoff1, payoff2, p_solution);
275276
}
276277
}
277278
else {
@@ -283,7 +284,7 @@ void NashLcpBehaviorSolver<T>::FillTableau(Matrix<T> &A, const GameNode &n, T pr
283284
snew++;
284285
A(ns1 + snew, infoset_idx) = static_cast<T>(1);
285286
A(infoset_idx, ns1 + snew) = static_cast<T>(-1);
286-
FillTableau(A, child, prob, s1, snew, p_solution);
287+
FillTableau(A, child, prob, s1, snew, payoff1, payoff2, p_solution);
287288
}
288289
}
289290
}

src/solvers/lp/lp.cc

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ template <class T> class GameData {
3434

3535
explicit GameData(const Game &);
3636

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

3939
void GetBehavior(MixedBehaviorProfile<T> &v, const Array<T> &, const Array<T> &,
4040
const GameNode &, int, int);
@@ -57,21 +57,22 @@ template <class T> GameData<T>::GameData(const Game &p_game) : minpay(p_game->Ge
5757
// Recursively fills the constraint matrix A for the subtree rooted at 'n'.
5858
//
5959
template <class T>
60-
void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2)
60+
void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, int s1, int s2,
61+
T payoff)
6162
{
6263
const GameOutcome outcome = n->GetOutcome();
6364
if (outcome) {
64-
A(s1, s2) +=
65-
Rational(prob) * (outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1)) - minpay);
65+
payoff += outcome->GetPayoff<Rational>(n->GetGame()->GetPlayer(1));
6666
}
6767
if (n->IsTerminal()) {
68+
A(s1, s2) += Rational(prob) * (payoff - minpay);
6869
return;
6970
}
7071
const GameInfoset infoset = n->GetInfoset();
7172
if (n->GetPlayer()->IsChance()) {
7273
for (const auto &action : infoset->GetActions()) {
7374
FillTableau(A, n->GetChild(action), prob * static_cast<T>(infoset->GetActionProb(action)),
74-
s1, s2);
75+
s1, s2, payoff);
7576
}
7677
}
7778
else if (n->GetPlayer()->GetNumber() == 1) {
@@ -80,7 +81,7 @@ void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, in
8081
A(s1, col) = static_cast<T>(1);
8182
for (const auto &child : n->GetChildren()) {
8283
A(++snew, col) = static_cast<T>(-1);
83-
FillTableau(A, child, prob, snew, s2);
84+
FillTableau(A, child, prob, snew, s2, payoff);
8485
}
8586
}
8687
else {
@@ -89,7 +90,7 @@ void GameData<T>::FillTableau(Matrix<T> &A, const GameNode &n, const T &prob, in
8990
A(row, s2) = static_cast<T>(-1);
9091
for (const auto &child : n->GetChildren()) {
9192
A(row, ++snew) = static_cast<T>(1);
92-
FillTableau(A, child, prob, s1, snew);
93+
FillTableau(A, child, prob, s1, snew, payoff);
9394
}
9495
}
9596
}
@@ -187,7 +188,7 @@ std::list<MixedBehaviorProfile<T>> LpBehaviorSolve(const Game &p_game,
187188
b = static_cast<T>(0);
188189
c = static_cast<T>(0);
189190

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

tests/test_nash.py

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -466,23 +466,20 @@ def test_lcp_behavior_double():
466466
games.create_2x2_zero_sum_efg(),
467467
[[["1/2", "1/2"]], [["1/2", "1/2"]]]
468468
),
469-
pytest.param(
469+
(
470470
games.create_2x2_zero_sum_efg(missing_term_outcome=True),
471471
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
472-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
473472
),
474473
(games.create_matching_pennies_efg(),
475474
[[["1/2", "1/2"]], [["1/2", "1/2"]]]),
476-
pytest.param(
475+
(
477476
games.create_matching_pennies_efg(with_neutral_outcome=True),
478477
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
479-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
480478
),
481479
(games.create_stripped_down_poker_efg(), [[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]]),
482-
pytest.param(
480+
(
483481
games.create_stripped_down_poker_efg(nonterm_outcomes=True),
484482
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
485-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
486483
),
487484
(
488485
games.create_kuhn_poker_efg(),
@@ -498,7 +495,7 @@ def test_lcp_behavior_double():
498495
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
499496
],
500497
),
501-
pytest.param(
498+
(
502499
games.create_kuhn_poker_efg(nonterm_outcomes=True),
503500
[
504501
[
@@ -511,7 +508,6 @@ def test_lcp_behavior_double():
511508
],
512509
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
513510
],
514-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
515511
),
516512
# In the next test case:
517513
# 1/2-1/2 for l/r is determined by MixedBehaviorProfile.UndefinedToCentroid()
@@ -534,13 +530,12 @@ def test_lcp_behavior_double():
534530
[["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]],
535531
]
536532
),
537-
pytest.param(
533+
(
538534
games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True),
539535
[
540536
[["1/3", 0, "2/3"], ["2/3", 0, "1/3"]],
541537
[["2/3", "1/3"], ["1/3", "2/3"], ["1/3", "2/3"]],
542538
],
543-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
544539
),
545540
(
546541
games.create_large_payoff_game_efg(),
@@ -557,13 +552,12 @@ def test_lcp_behavior_double():
557552
[[1, 0], ["6/11", "5/11"]]
558553
]
559554
),
560-
pytest.param(
555+
(
561556
games.create_chance_in_middle_efg(nonterm_outcomes=True),
562557
[
563558
[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]],
564559
[[1, 0], ["6/11", "5/11"]]
565560
],
566-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
567561
),
568562
# Non-zero-sum games
569563
(
@@ -586,19 +580,17 @@ def test_lcp_behavior_double():
586580
games.create_entry_accomodation_efg(),
587581
[[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]]
588582
),
589-
pytest.param(
583+
(
590584
games.create_entry_accomodation_efg(nonterm_outcomes=True),
591585
[[["2/3", "1/3"], [1, 0], [1, 0]], [["2/3", "1/3"]]],
592-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
593586
),
594587
(
595588
games.create_non_zero_sum_lacking_outcome_efg(),
596589
[[["1/3", "2/3"]], [["1/2", "1/2"]]]
597590
),
598-
pytest.param(
591+
(
599592
games.create_non_zero_sum_lacking_outcome_efg(missing_term_outcome=True),
600593
[[["1/3", "2/3"]], [["1/2", "1/2"]]],
601-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
602594
),
603595
],
604596
)
@@ -677,35 +669,31 @@ def test_lp_behavior_double():
677669
games.create_two_player_perfect_info_win_lose_efg(),
678670
[[[0, 1], [1, 0]], [[1, 0], [1, 0]]],
679671
),
680-
pytest.param(
672+
(
681673
games.create_two_player_perfect_info_win_lose_efg(nonterm_outcomes=True),
682674
[[[0, 1], [1, 0]], [[1, 0], [1, 0]]],
683-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
684675
),
685676
(
686677
games.create_2x2_zero_sum_efg(missing_term_outcome=False),
687678
[[["1/2", "1/2"]], [["1/2", "1/2"]]]
688679
),
689-
pytest.param(
680+
(
690681
games.create_2x2_zero_sum_efg(missing_term_outcome=True),
691682
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
692-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
693683
),
694684
(games.create_matching_pennies_efg(with_neutral_outcome=False),
695685
[[["1/2", "1/2"]], [["1/2", "1/2"]]]),
696-
pytest.param(
686+
(
697687
games.create_matching_pennies_efg(with_neutral_outcome=True),
698688
[[["1/2", "1/2"]], [["1/2", "1/2"]]],
699-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
700689
),
701690
(
702691
games.create_stripped_down_poker_efg(),
703692
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
704693
),
705-
pytest.param(
694+
(
706695
games.create_stripped_down_poker_efg(nonterm_outcomes=True),
707696
[[[1, 0], ["1/3", "2/3"]], [["2/3", "1/3"]]],
708-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
709697
),
710698
(
711699
games.create_kuhn_poker_efg(),
@@ -714,7 +702,7 @@ def test_lp_behavior_double():
714702
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
715703
],
716704
),
717-
pytest.param(
705+
(
718706
games.create_kuhn_poker_efg(nonterm_outcomes=True),
719707
[
720708
[
@@ -727,7 +715,6 @@ def test_lp_behavior_double():
727715
],
728716
[[1, 0], ["2/3", "1/3"], [0, 1], [0, 1], ["2/3", "1/3"], [1, 0]],
729717
],
730-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
731718
),
732719
(
733720
games.create_seq_form_STOC_paper_zero_sum_2_player_efg(),
@@ -747,13 +734,12 @@ def test_lp_behavior_double():
747734
[["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]],
748735
]
749736
),
750-
pytest.param(
737+
(
751738
games.create_three_action_internal_outcomes_efg(nonterm_outcomes=True),
752739
[
753740
[["1/3", 0, "2/3"], ["2/3", 0, "1/3"]],
754741
[["2/3", "1/3"], ["2/3", "1/3"], ["1/3", "2/3"]],
755742
],
756-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
757743
),
758744
(
759745
games.create_large_payoff_game_efg(),
@@ -770,13 +756,12 @@ def test_lp_behavior_double():
770756
[[1, 0], ["6/11", "5/11"]]
771757
],
772758
),
773-
pytest.param(
759+
(
774760
games.create_chance_in_middle_efg(nonterm_outcomes=True),
775761
[
776762
[["3/11", "8/11"], [1, 0], [1, 0], [1, 0], [1, 0]],
777763
[[1, 0], ["6/11", "5/11"]]
778764
],
779-
marks=pytest.mark.xfail(reason="Problem with non-standard outcomes")
780765
),
781766
],
782767
)

0 commit comments

Comments
 (0)