diff --git a/ChangeLog b/ChangeLog index 90effad052..57cbe6baab 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,19 @@ +## [17.0.0-alpha.2] - unreleased + +### Added +- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047) + +### Fixed +- `Game.add_player`, on a game in strategic form, no longer leaves the new player's sole + strategy with an empty label; it is now labeled `"1"`, as `Game.new_table` already labels + every other player's initial strategies. (#1047) +- Numerical data in action graph games is now read with exact precision, to match other game formats. (#879) + +### Removed +- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces + nonempty, unique labels. (#1047) + + ## [17.0.0-alpha.1] - 2026-08-13 ### Added @@ -11,7 +27,6 @@ its actions to be specified up front, just as `Game.make_event` does. (#999) - `Game.get_behavior` returns a map-like view of the actions a reduced strategy prescribes at its player's information sets; the unreachable information sets are absent. (#1002) -- Added `Game.relabel_strategies`, which simultaneously reassigns the labels of a player's strategies. (#1047) ### Changed - Adopted the C++20 standard; build system now enforces exactly C++20 (not a minimum) (cf. #1027) @@ -60,9 +75,6 @@ ### Fixed - Converting a behavior profile to a strategy profile, and computing pure-strategy payoffs no longer inserts spurious entries into a strategy's internal action map. -- `Game.add_player`, on a game in strategic form, no longer leaves the new player's sole - strategy with an empty label; it is now labeled `"1"`, as `Game.new_table` already labels - every other player's initial strategies. (#1047) ### Removed - `Game.set_player` has been removed; use `Game.make_infoset`, specifying the members of the @@ -87,8 +99,6 @@ stub overrides it forced on the strategic, AGG, and BAGG representations; it was subsumed by `MakeEvent`, and by this point was used only in support of the GUI's "Edit Move" dialog, which now builds the equivalent `MakeEvent` call directly. (#999) -- Assigning to `Strategy.label` has been removed; use `Game.relabel_strategies`, which enforces - nonempty, unique labels. (#1047) ## [16.7.0] - 2026-07-11 diff --git a/src/games/agg/agg.cc b/src/games/agg/agg.cc index 3d60148096..05be8d684e 100644 --- a/src/games/agg/agg.cc +++ b/src/games/agg/agg.cc @@ -24,6 +24,7 @@ #include #include #include +#include #include "games/agg/gray.h" #include "games/agg/agg.h" @@ -35,13 +36,15 @@ inline int select2nd(const pair &x) { return x.second; } AGG::AGG(int numPlayers, std::vector &_actions, int numANodes, int _numPNodes, vector> &_actionSets, vector> &neighb, - vector &projTypes, vector> &projS, - vector>> &proj, vector> &projF, - vector>> &Po, vector &P, vector &_payoffs) + vector &projTypes, vector>> &projS, + vector>> &proj, vector> &projF, + vector>> &Po, vector &_payoffs, + vector &_exactPayoffs) : numPlayers(numPlayers), numActionNodes(numANodes), numPNodes(_numPNodes), actionSets(_actionSets), neighbors(neighb), projectionTypes(projTypes), payoffs(_payoffs), - projection(proj), projectedStrat(projS), fullProjectedStrat(projS), projFunctions(projF), - Porder(Po), Pr(P), isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), + exactPayoffs(_exactPayoffs), projection(proj), m_state(numANodes, numPlayers), + m_exactState(numANodes, numPlayers), fullProjectedStrat(projS), projFunctions(projF), + Porder(Po), isPure(numANodes, true), node2Action(numANodes, vector(numPlayers)), cache(numPlayers + 1), player2Class(numPlayers), kSymStrategyOffset(1, 0) { @@ -220,8 +223,8 @@ std::shared_ptr AGG::makeAGG(istream &in) projTypes[i] = make_proj_func((TypeEnum)pt, in, S, P); } - vector> projS; - vector>> proj; + vector>> projS; + vector>> proj; setProjections(projS, proj, n, S, P, ASets, neighb, projTypes); vector> projF(S); @@ -235,8 +238,9 @@ std::shared_ptr AGG::makeAGG(istream &in) } vector>> Po(n); - vector Pr(n); - vector pays(S); // payoffs + vector> Pr(n); + vector pays(S); // payoffs + vector exactPays(S); // exact (arbitrary-precision) counterpart of pays set> doneASets; for (i = 0; i < n; i++) { @@ -279,22 +283,23 @@ std::shared_ptr AGG::makeAGG(istream &in) } switch (t) { case COMPLETE: - AGG::makeCOMPLETEpayoff(in, pays[i]); + AGG::makeCOMPLETEpayoff(in, pays[i], exactPays[i]); break; case MAPPING: - AGG::makeMAPPINGpayoff(in, pays[i], neighb[i].size()); + AGG::makeMAPPINGpayoff(in, pays[i], exactPays[i], neighb[i].size()); break; case ADDITIVE: default: throw std::runtime_error("Unknown payoff type " + std::to_string(t)); } } - return std::make_shared(n, size, S, P, ASets, neighb, projTypes, projS, proj, projF, Po, Pr, - pays); + return std::make_shared(n, size, S, P, ASets, neighb, projTypes, projS, proj, projF, Po, + pays, exactPays); } -void AGG::setProjections(vector> &projS, vector>> &proj, - int N, int S, int P, vector> &AS, vector> &neighb, +void AGG::setProjections(vector>> &projS, + vector>> &proj, int N, int S, int P, + vector> &AS, vector> &neighb, vector &projTypes) { int Node, i, j, k, numNei, actions; @@ -370,7 +375,7 @@ void AGG::getAn(multiset &dest, vector> &neighb, vector &Po, int i, int N, vector &projS) +void AGG::initPorder(vector &Po, int i, int N, vector> &projS) { vector> order; int k; @@ -387,132 +392,186 @@ void AGG::initPorder(vector &Po, int i, int N, vector &projS) transform(order.begin(), order.end(), p, select2nd); } -// compute the induced distribution -void AGG::computeP(int player, int act, int player2, int act2) +// compute the induced distribution; only the working state (state()) varies with V, the +// action graph structure itself is shared. +template void AGG::computeP(int player, int act, int player2, int act2) { + auto &s = state(); // apply player's strat - Pr[0].reset(); - Pr[0].insert(make_pair(projection[actionSets[player][act]][player][act], 1.0)); + s.Pr[0].reset(); + s.Pr[0].insert(make_pair(projection[actionSets[player][act]][player][act], V(1))); const int numNei = neighbors[actionSets[player][act]].size(); // apply others' strat for (int k = 1; k < numPlayers; k++) { - Pr[k].reset(); + s.Pr[k].reset(); if (Porder[player][act][k] == player2) { if (act2 == -1) { - // Pr[k].swap(Pr[k-1]); - Pr[k] = Pr[k - 1]; + s.Pr[k] = s.Pr[k - 1]; } else { // apply player2's pure strat - aggdistrib temp; - temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], 1.0)); - Pr[k].multiply(Pr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); + ConfigDistribution temp; + temp.insert(make_pair(projection[actionSets[player][act]][player2][act2], V(1))); + s.Pr[k].multiply(s.Pr[k - 1], temp, numNei, projFunctions[actionSets[player][act]]); } } else { - Pr[k].multiply(Pr[k - 1], projectedStrat[actionSets[player][act]][Porder[player][act][k]], - numNei, projFunctions[actionSets[player][act]]); + s.Pr[k].multiply(s.Pr[k - 1], + s.projectedStrat[actionSets[player][act]][Porder[player][act][k]], numNei, + projFunctions[actionSets[player][act]]); } } } -void AGG::doProjection(int Node, AggNumber *s) +template void AGG::doProjection(int Node, V *s) { for (int i = 0; i < numPlayers; i++) { doProjection(Node, i, &(s[firstAction(i)])); } } -void AGG::doProjection(int Node, int i, AggNumber *s) +template void AGG::doProjection(int Node, int i, V *s) { - projectedStrat[Node][i].reset(); - for (int j = 0; j < actions[i]; j++) { - if (s[j] > (AggNumber)0.0) { - projectedStrat[Node][i] += make_pair(projection[Node][i][j], s[j]); - } - } + state().project(Node, i, projection[Node][i], actions[i], s); } -AggNumber AGG::getPurePayoff(int player, const std::vector &s) +template void AGG::computeP(int player, int act, int player2, int act2); +template void AGG::computeP(int player, int act, int player2, int act2); +template void AGG::doProjection(int Node, double *s); +template void AGG::doProjection(int Node, Rational *s); +template void AGG::doProjection(int Node, int i, double *s); +template void AGG::doProjection(int Node, int i, Rational *s); + +template V AGG::getPurePayoff(int player, const std::vector &s) const { assert(player >= 0 && player < numPlayers); const int Node = actionSets[player][s[player]]; const int keylen = neighbors[Node].size(); - config pureprofile(projection[Node][0][s[0]]); + Config pureprofile(projection[Node][0][s[0]]); for (int i = 1; i < numPlayers; i++) { for (int j = 0; j < keylen; j++) { pureprofile[j] = (*projFunctions[Node][j])(pureprofile[j], projection[Node][i][s[i]][j]); } } - auto p = payoffs[Node].find(pureprofile); - if (p == payoffs[Node].end()) { + const auto throwNotFound = [&]() -> void { std::stringstream str; str << "AGG::getPurePayoff ERROR: unable to find the following configuration "; str << "["; copy(pureprofile.begin(), pureprofile.end(), ostream_iterator(str, " ")); str << "] in payoffs of action node #" << Node; throw std::runtime_error(str.str()); + }; + if constexpr (std::is_same_v) { + auto p = exactPayoffs[Node].find(pureprofile); + if (p == exactPayoffs[Node].end()) { + throwNotFound(); + } + return static_cast(p->second); + } + else { + auto p = payoffs[Node].find(pureprofile); + if (p == payoffs[Node].end()) { + throwNotFound(); + } + return p->second; } - return p->second; } -AggNumber AGG::getMixedPayoff(int player, StrategyProfile &s) +template double AGG::getPurePayoff(int player, const std::vector &s) const; +template Rational AGG::getPurePayoff(int player, const std::vector &s) const; + +Rational AGG::exactInnerProd(int node, const ConfigDistribution &dist) const { - AggNumber result = 0.0; - assert(player >= 0 && player < numPlayers); - for (int act = 0; act < actions[player]; ++act) { - if (s[act + firstAction(player)] > (AggNumber)0.0) { - result += s[act + firstAction(player)] * getV(player, act, s); + Rational result(0); + for (const auto &entry : dist) { + const auto it = exactPayoffs[node].find(entry.first); + if (it != exactPayoffs[node].end() && entry.second != Rational(0)) { + result += entry.second * static_cast(it->second); } } return result; } -void AGG::getPayoffVector(AggNumberVector &dest, int player, const StrategyProfile &s) +template V AGG::payoffInnerProd(int node, const ConfigDistribution &dist) { - assert(player >= 0 && player < numPlayers); - for (int act = 0; act < actions[player]; ++act) { - dest[act] = getV(player, act, s); + if constexpr (std::is_same_v) { + return exactInnerProd(node, dist); + } + else { + return dist.inner_prod(payoffs[node]); } } -AggNumber AGG::getV(int player, int act, const StrategyProfile &s) +template V AGG::getV(int player, int act, const StrategyProfile &s) { - // project s to the projectedStrat doProjection(actionSets.at(player).at(act), s); - computeP(player, act); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player][act]]); + computeP(player, act); + return payoffInnerProd(actionSets[player][act], state().Pr[numPlayers - 1]); } -AggNumber AGG::getJ(int player1, int act1, int player2, int act2, StrategyProfile &s) +template +V AGG::getJ(int player1, int act1, int player2, int act2, const StrategyProfile &s) { doProjection(actionSets[player1][act1], s); - computeP(player1, act1, player2, act2); - return Pr[numPlayers - 1].inner_prod(payoffs[actionSets[player1][act1]]); + computeP(player1, act1, player2, act2); + return payoffInnerProd(actionSets[player1][act1], state().Pr[numPlayers - 1]); +} + +template double AGG::payoffInnerProd(int node, const ConfigDistribution &dist); +template Rational AGG::payoffInnerProd(int node, + const ConfigDistribution &dist); + +template V AGG::getMixedPayoff(int player, const StrategyProfile &s) +{ + V result(0); + assert(player >= 0 && player < numPlayers); + for (int act = 0; act < actions[player]; ++act) { + if (s[act + firstAction(player)] > V(0)) { + result += s[act + firstAction(player)] * getV(player, act, s); + } + } + return result; } +void AGG::getPayoffVector(std::vector &dest, int player, const StrategyProfile &s) +{ + assert(player >= 0 && player < numPlayers); + for (int act = 0; act < actions[player]; ++act) { + dest[act] = getV(player, act, s); + } +} + +template double AGG::getV(int player, int act, const StrategyProfile &s); +template Rational AGG::getV(int player, int act, const StrategyProfile &s); +template double AGG::getJ(int player1, int act1, int player2, int act2, + const StrategyProfile &s); +template Rational AGG::getJ(int player1, int act1, int player2, int act2, + const StrategyProfile &s); +template double AGG::getMixedPayoff(int player, const StrategyProfile &s); +template Rational AGG::getMixedPayoff(int player, const StrategyProfile &s); + // getSymMixedPayoff: compute expected payoff under a symmetric mixed strat, // for a symmetric game. // parameter: s is the mixed strategy of one player. It is a vector of // probabilities, indexed by the action node. -AggNumber AGG::getSymMixedPayoff(StrategyProfile &s) +double AGG::getSymMixedPayoff(StrategyProfile &s) { - AggNumber result = 0; + double result = 0; if (!isSymmetric()) { throw std::runtime_error("AGG::getSymMixedPayoff: the game is not symmetric"); } for (int node = 0; node < numActionNodes; ++node) { - if (s[node] > (AggNumber)0.0) { + if (s[node] > (double)0.0) { result += s[node] * getSymMixedPayoff(node, s); } } return result; } -void AGG::getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s) +void AGG::getSymPayoffVector(std::vector &dest, StrategyProfile &s) { if (!isSymmetric()) { throw std::runtime_error("AGG::getSymMixedPayoff: the game is not symmetric"); @@ -522,48 +581,49 @@ void AGG::getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s) } } -AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) +double AGG::getSymMixedPayoff(int node, StrategyProfile &s) { const int numNei = neighbors[node].size(); if (!isPure[node]) { // then compute EU using trie_map::power() doProjection(node, 0, s); assert(numPlayers > 1); - // aggdistrib *dest; - // projectedStrat[node][0].power(numPlayers-1, dest, Pr, numNei,projFunctions[node]); - aggdistrib &dest = Pr[numPlayers - 1]; - projectedStrat[node][0].power(numPlayers - 1, dest, Pr[numPlayers - 2], numNei, - projFunctions[node]); + // ConfigDistribution *dest; + // m_state.projectedStrat[node][0].power(numPlayers-1, dest, m_state.Pr, + // numNei,projFunctions[node]); + ConfigDistribution &dest = m_state.Pr[numPlayers - 1]; + m_state.projectedStrat[node][0].power(numPlayers - 1, dest, m_state.Pr[numPlayers - 2], numNei, + projFunctions[node]); return dest.inner_prod(projection[node][0][node], numNei, projFunctions[node], payoffs[node]); } - AggNumber V = 0.0; + double V = 0.0; vector support; - AggNumber null_prob = 1; + double null_prob = 1; // do projection & get support int self = -1; for (int i = 0; i < numNei; ++i) { if (neighbors[node][i] == node) { self = i; } - if (s[neighbors[node][i]] > (AggNumber)0) { + if (s[neighbors[node][i]] > (double)0) { support.push_back(i); null_prob -= s[neighbors[node][i]]; } } - if (numNei < numActionNodes && null_prob > (AggNumber)0) { + if (numNei < numActionNodes && null_prob > (double)0) { support.push_back(-1); } // gray code GrayComposition gc(numPlayers - 1, support.size()); - AggNumber prob = - pow((support.at(0) >= 0) ? s[neighbors[node][support[0]]] : null_prob, numPlayers - 1); + double prob = + std::pow((support.at(0) >= 0) ? s[neighbors[node][support[0]]] : null_prob, numPlayers - 1); while (true) { const vector &comp = gc.get(); - config c(numNei, 0); + Config c(numNei, 0); for (size_t j = 0; j < support.size(); ++j) { if (support[j] != -1) { c[support[j]] = comp[j]; @@ -581,13 +641,10 @@ AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) break; } // update prob - const AggNumber i_prob = - (support.at(gc.i) != -1) ? s[neighbors[node][support[gc.i]]] : null_prob; - const AggNumber d_prob = - (support.at(gc.d) != -1) ? s[neighbors[node][support[gc.d]]] : null_prob; - assert(i_prob > (AggNumber)0 && d_prob > (AggNumber)0); - prob *= - ((AggNumber)(gc.get().at(gc.d) + 1)) * i_prob / (AggNumber)(gc.get().at(gc.i)) / d_prob; + const double i_prob = (support.at(gc.i) != -1) ? s[neighbors[node][support[gc.i]]] : null_prob; + const double d_prob = (support.at(gc.d) != -1) ? s[neighbors[node][support[gc.d]]] : null_prob; + assert(i_prob > (double)0 && d_prob > (double)0); + prob *= ((double)(gc.get().at(gc.d) + 1)) * i_prob / (double)(gc.get().at(gc.i)) / d_prob; } // end while @@ -598,8 +655,8 @@ AggNumber AGG::getSymMixedPayoff(int node, StrategyProfile &s) // plClass: the index for the player class // s: mixed strat for that player class -void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, - aggdistrib &dest, int plClass2, int act2) +void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, + ConfigDistribution &dest, int plClass2, int act2) { const int node = uniqueActionSets.at(ownPlClass).at(act); int numPl = playerClasses.at(plClass).size(); @@ -616,17 +673,18 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int if (!isPure[node]) { const int player = playerClasses[plClass].at(0); - projectedStrat[node][player].reset(); + m_state.projectedStrat[node][player].reset(); if (numPl > 0) { for (int j = 0; j < actions[player]; j++) { - if (s[j] > (AggNumber)0.0) { - projectedStrat[node][player] += make_pair(projection[node][player][j], s[j]); + if (s[j] > (double)0.0) { + m_state.projectedStrat[node][player] += make_pair(projection[node][player][j], s[j]); } } - projectedStrat[node][player].power(numPl, dest, Pr[0], numNei, projFunctions[node]); + m_state.projectedStrat[node][player].power(numPl, dest, m_state.Pr[0], numNei, + projFunctions[node]); } if (plClass == ownPlClass) { - aggdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[node][player].at(act), 1.0)); if (dest.size() > 0) { dest.multiply(temp, numNei, projFunctions[node]); @@ -637,7 +695,7 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int } } if (plClass == plClass2) { - aggdistrib temp; + ConfigDistribution temp; temp.insert(make_pair(projection[node][player].at(act2), 1.0)); if (dest.size() > 0) { dest.multiply(temp, numNei, projFunctions[node]); @@ -650,9 +708,9 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int return; } - // AggNumber V = 0.0; + // double V = 0.0; vector support; - AggNumber null_prob = 1; + double null_prob = 1; // do projection & get support int self = -1; // index of self in the neighbor list int ind2 = -1; // index of act2 in the neighbor list @@ -666,25 +724,25 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int } const int a = node2Action.at(neighbors[node][i]).at(p); - if (a >= 0 && s[a] > (AggNumber)0) { + if (a >= 0 && s[a] > (double)0) { support.push_back(i); null_prob -= s[a]; } } - if (null_prob > (AggNumber)0) { + if (null_prob > (double)0) { support.push_back(-1); } // gray code GrayComposition gc(numPl, support.size()); - const AggNumber prob0 = + const double prob0 = (support.at(0) >= 0) ? s[node2Action[neighbors[node].at(support[0])][p]] : null_prob; - AggNumber prob = pow(prob0, numPl); + double prob = std::pow(prob0, numPl); while (true) { const vector &comp = gc.get(); - config c(numNei, 0); + Config c(numNei, 0); for (size_t j = 0; j < support.size(); ++j) { if (support[j] != -1) { c[support[j]] = comp[j]; @@ -708,23 +766,22 @@ void AGG::getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int break; } // update prob - const AggNumber i_prob = + const double i_prob = (support.at(gc.i) != -1) ? s[node2Action[neighbors[node][support[gc.i]]][p]] : null_prob; - const AggNumber d_prob = + const double d_prob = (support.at(gc.d) != -1) ? s[node2Action[neighbors[node][support[gc.d]]][p]] : null_prob; - assert(i_prob > (AggNumber)0 && d_prob > (AggNumber)0); - prob *= - ((AggNumber)(gc.get().at(gc.d) + 1)) * i_prob / (AggNumber)(gc.get().at(gc.i)) / d_prob; + assert(i_prob > (double)0 && d_prob > (double)0); + prob *= ((double)(gc.get().at(gc.d) + 1)) * i_prob / (double)(gc.get().at(gc.i)) / d_prob; } // end while } -AggNumber AGG::getKSymMixedPayoff(int playerClass, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, vector> &s) { - AggNumber result = 0.0; + double result = 0.0; for (int act = 0; act < (int)uniqueActionSets[playerClass].size(); act++) { - if (s[playerClass][act] > (AggNumber)0.0) { + if (s[playerClass][act] > (double)0.0) { result += s[playerClass][act] * getKSymMixedPayoff(playerClass, act, s); } @@ -732,12 +789,12 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, vector &s) return result; } -AggNumber AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) +double AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) { - AggNumber result = 0.0; + double result = 0.0; for (int act = 0; act < (int)uniqueActionSets[playerClass].size(); act++) { - if (s[firstKSymAction(playerClass) + act] > (AggNumber)0.0) { + if (s[firstKSymAction(playerClass) + act] > (double)0.0) { result += s[firstKSymAction(playerClass) + act] * getKSymMixedPayoff(s, playerClass, act); } @@ -745,21 +802,22 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, StrategyProfile &s) return result; } -void AGG::getKSymPayoffVector(AggNumberVector &dest, int playerClass, StrategyProfile &s) +void AGG::getKSymPayoffVector(std::vector &dest, int playerClass, + StrategyProfile &s) { for (size_t act = 0; act < uniqueActionSets[playerClass].size(); ++act) { dest[act] = getKSymMixedPayoff(s, playerClass, act); } } -AggNumber AGG::getKSymMixedPayoff(int playerClass, int act, vector &s) +double AGG::getKSymMixedPayoff(int playerClass, int act, vector> &s) { const int numPC = playerClasses.size(); const int numNei = neighbors[uniqueActionSets[playerClass][act]].size(); - static aggdistrib d, temp; + static ConfigDistribution d, temp; d.reset(); temp.reset(); getSymConfigProb(0, s[0], playerClass, act, d); @@ -770,18 +828,18 @@ AggNumber AGG::getKSymMixedPayoff(int playerClass, int act, vector &s, int pClass1, int act1, + int pClass2, int act2) { const int numPC = playerClasses.size(); const int numNei = neighbors[uniqueActionSets[pClass1][act1]].size(); - static aggdistrib d, temp; + static ConfigDistribution d, temp; if (pClass2 >= 0 && pClass1 == pClass2 && playerClasses.at(pClass1).size() <= 1) { return 0; } d.reset(); temp.reset(); - StrategyProfile s0(getNumKSymActions(0), 0.0); + StrategyProfile s0(getNumKSymActions(0), 0.0); // if (0==pClass2) s0[act2]=1; // else for (int a = firstKSymAction(0); a < lastKSymAction(0); ++a) { @@ -789,7 +847,7 @@ AggNumber AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act } getSymConfigProb(0, s0, pClass1, act1, d, pClass2, act2); for (int pc = 1; pc < numPC; pc++) { - StrategyProfile ss(getNumKSymActions(pc), 0.0); + StrategyProfile ss(getNumKSymActions(pc), 0.0); // if (pc==pClass2)ss[act2]=1; // else for (int a = 0; a < getNumKSymActions(pc); ++a) { @@ -801,12 +859,12 @@ AggNumber AGG::getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act return d.inner_prod(payoffs[uniqueActionSets[pClass1][act1]]); } -void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) +void AGG::makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay, + int numNei) { int num; char c; - AggNumber u; - aggpayoff temp; + PayoffTable temp; // temp.swap(pay); temp = pay; pay.clear(); @@ -850,16 +908,19 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) " expected. Instead, got " + std::to_string(c)); } - in >> u; // get payoff + std::string word; + in >> word; // get payoff if (!in.good()) { std::stringstream str; str << "Error trying to read the utility value for configuration "; copy(key.begin(), key.end(), ostream_iterator(str, " ")); throw std::runtime_error(str.str()); } + const Number num(word); + const auto u = static_cast(num); // insert - const pair::iterator, bool> r = pay.insert(make_pair(key, u)); + const pair::iterator, bool> r = pay.insert(make_pair(key, u)); if (!r.second) { std::stringstream str; str << "ERROR: overwriting utility at ["; @@ -867,6 +928,13 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) str << "]"; throw std::runtime_error(str.str()); } + if (!exactPay.insert(make_pair(key, num)).second) { + std::stringstream str; + str << "ERROR: overwriting utility at ["; + copy(key.begin(), key.end(), ostream_iterator(str, " ")); + str << "]"; + throw std::runtime_error(str.str()); + } } // check for (auto &it : temp) { @@ -880,28 +948,55 @@ void AGG::makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int numNei) } } -AggNumber AGG::getMaxPayoff() const +template V AGG::getMaxPayoff() const { assert(numActionNodes > 0); - AggNumber result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { - for (const auto &it : payoffs[i]) { - result = max(result, it.second); + if constexpr (std::is_same_v) { + Rational result = static_cast(exactPayoffs[0].begin()->second); + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + result = max(result, static_cast(it.second)); + } } + return result; + } + else { + double result = payoffs[0].begin()->second; + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : payoffs[i]) { + result = max(result, it.second); + } + } + return result; } - return result; } -AggNumber AGG::getMinPayoff() const +template V AGG::getMinPayoff() const { assert(numActionNodes > 0); - AggNumber result = payoffs[0].begin()->second; - for (int i = 1; i < numActionNodes; i++) { - for (const auto &it : payoffs[i]) { - result = min(result, it.second); + if constexpr (std::is_same_v) { + Rational result = static_cast(exactPayoffs[0].begin()->second); + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : exactPayoffs[i]) { + result = min(result, static_cast(it.second)); + } } + return result; + } + else { + double result = payoffs[0].begin()->second; + for (int i = 0; i < numActionNodes; i++) { + for (const auto &it : payoffs[i]) { + result = min(result, it.second); + } + } + return result; } - return result; } +template double AGG::getMaxPayoff() const; +template Rational AGG::getMaxPayoff() const; +template double AGG::getMinPayoff() const; +template Rational AGG::getMinPayoff() const; + } // namespace Gambit::agg diff --git a/src/games/agg/agg.h b/src/games/agg/agg.h index efe01cb560..8d0d42e65b 100644 --- a/src/games/agg/agg.h +++ b/src/games/agg/agg.h @@ -25,10 +25,13 @@ #define GAMBIT_AGG_AGG_H #include +#include #include #include +#include #include "proj_func.h" #include "trie_map.h" +#include "games/number.h" namespace Gambit { @@ -38,24 +41,60 @@ class aggame; namespace agg { -// data structure for mixed strategy profile -using AggNumber = double; -using StrategyProfile = std::vector; -using AggNumberVector = std::vector; +// mixed strategy profile: one probability per action; V is double or Rational. +template using StrategyProfile = std::vector; -// data structure for payoff function: -using aggpayoff = trie_map; +// payoff function for one action node (neighbor configuration -> payoff). Not a template like +// ConfigDistribution below: it's consumed via trie_map's own (floating-point) inner_prod. +using PayoffTable = trie_map; -// data struct for prob distribution over configurations: -using aggdistrib = trie_map; +// exact counterpart of PayoffTable. A std::map, not a trie_map: it stores Number +// (not Rational) to preserve the original source text, and trie_map's inner_prod has an +// epsilon-discard threshold that is unsound for exact arithmetic. +using ExactPayoffTable = std::map, Number>; + +// probability distribution over configurations; V is double or Rational. Templated (unlike +// PayoffTable/ExactPayoffTable) because it participates in convolution arithmetic. +template using ConfigDistribution = trie_map; + +// AGG's mutable working state for the convolution algorithm. AGG keeps one instance per V (see +// AGG::state()) since double and Rational profiles may be in use simultaneously. +template struct ConvolutionState { + // for each action node and player, the projected mixed strategy (a distribution over + // 'contributions' -- see AGG::projection). + std::vector>> projectedStrat; + + // used by AGG::computeP(): the distribution induced by a partial strategy profile, or (for + // the payoff jacobian) by all agents except one. + std::vector> Pr; + + ConvolutionState(int numActionNodes, int numPlayers) + : projectedStrat(numActionNodes, std::vector>(numPlayers)), + Pr(numPlayers) + { + } + + // set projectedStrat[Node][player] to the weighted sum of contributions[j] over actions with + // s[j] > 0. + void project(int Node, int player, const std::vector> &contributions, + int numActions, const V *s) + { + projectedStrat[Node][player].reset(); + for (int j = 0; j < numActions; j++) { + if (s[j] > V(0)) { + projectedStrat[Node][player] += std::make_pair(contributions[j], s[j]); + } + } + } +}; // types of input formats for payoff func -using payofftype = enum { COMPLETE, MAPPING, ADDITIVE }; +using PayoffType = enum { COMPLETE, MAPPING, ADDITIVE }; class AGG { public: - using config = std::vector; + using Config = std::vector; using ActionSet = std::vector; using PlayerSet = std::vector; @@ -68,13 +107,15 @@ class AGG { // read an AGG from input stream static std::shared_ptr makeAGG(std::istream &in); - // constructor + // constructor; projS seeds fullProjectedStrat only, not the convolution engine's own working + // state, which is sized independently. AGG(int numPlayers, std::vector &actions, int numANodes, int numPNodes, std::vector> &actionSets, std::vector> &neighbors, - std::vector &projTypes, std::vector> &projS, - std::vector>> &proj, + std::vector &projTypes, + std::vector>> &projS, + std::vector>> &proj, std::vector> &projF, std::vector>> &Po, - std::vector &P, std::vector &payoffs); + std::vector &payoffs, std::vector &exactPayoffs); ~AGG() = default; @@ -94,13 +135,16 @@ class AGG { int firstKSymAction(int i) const { return kSymStrategyOffset[i]; } int lastKSymAction(int i) const { return kSymStrategyOffset[i + 1]; } - // exp. payoff under mixed strat profile - AggNumber getMixedPayoff(int player, StrategyProfile &s); - void getPayoffVector(AggNumberVector &dest, int player, const StrategyProfile &s); - AggNumber getV(int player, int action, const StrategyProfile &s); - AggNumber getJ(int player, int action, int player2, int action2, StrategyProfile &s); + // expected payoff under a mixed profile, via the convolution algorithm. + template V getMixedPayoff(int player, const StrategyProfile &s); + void getPayoffVector(std::vector &dest, int player, const StrategyProfile &s); + template V getV(int player, int action, const StrategyProfile &s); + template + V getJ(int player, int action, int player2, int action2, const StrategyProfile &s); - AggNumber getPurePayoff(int player, const std::vector &s); + // payoff of the pure profile s: a direct table lookup. V is never deducible from s, so callers + // must specify it explicitly, e.g. getPurePayoff(...). + template V getPurePayoff(int player, const std::vector &s) const; bool isSymmetric() const { @@ -111,19 +155,19 @@ class AGG { } return true; } - AggNumber getSymMixedPayoff(StrategyProfile &s); - AggNumber getSymMixedPayoff(int actnode, StrategyProfile &s); - void getSymPayoffVector(AggNumberVector &dest, StrategyProfile &s); - AggNumber getKSymMixedPayoff(int playerClass, std::vector &s); - AggNumber getKSymMixedPayoff(int playerClass, StrategyProfile &s); - AggNumber getKSymMixedPayoff(int playerClass, int act, std::vector &s); - AggNumber getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, int pClass2 = -1, - int act2 = -1); - void getKSymPayoffVector(AggNumberVector &dest, int playerClass, StrategyProfile &s); - - AggNumberVector getExpectedConfig(StrategyProfile &s) + double getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(int actnode, StrategyProfile &s); + void getSymPayoffVector(std::vector &dest, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, std::vector> &s); + double getKSymMixedPayoff(int playerClass, StrategyProfile &s); + double getKSymMixedPayoff(int playerClass, int act, std::vector> &s); + double getKSymMixedPayoff(const StrategyProfile &s, int pClass1, int act1, + int pClass2 = -1, int act2 = -1); + void getKSymPayoffVector(std::vector &dest, int playerClass, StrategyProfile &s); + + std::vector getExpectedConfig(StrategyProfile &s) { - AggNumberVector res(numActionNodes, 0); + std::vector res(numActionNodes, 0); for (int i = 0; i < numPlayers; ++i) { for (int j = 0; j < actions[i]; ++j) { res[actionSets[i][j]] += s[firstAction(i) + j]; @@ -137,17 +181,22 @@ class AGG { { return Porder.at(player).at(action); } - const std::vector> &getProjection(int node) { return projection.at(node); } + const std::vector> &getProjection(int node) { return projection.at(node); } const std::vector &getActionSet(int player) { return actionSets.at(player); } - const aggpayoff &getPayoffMap(int node) { return payoffs.at(node); } + const PayoffTable &getPayoffMap(int node) { return payoffs.at(node); } - AggNumber getMaxPayoff() const; - AggNumber getMinPayoff() const; + // largest/smallest payoff in any pure profile; callers write getMaxPayoff(), etc. + template V getMaxPayoff() const; + template V getMinPayoff() const; void printPayoffs(std::ostream &s, int node) const { - s << payoffs.at(node).size() << std::endl; - s << payoffs[node]; + s << exactPayoffs.at(node).size() << std::endl; + for (const auto &entry : exactPayoffs.at(node)) { + s << "[ "; + std::copy(entry.first.begin(), entry.first.end(), std::ostream_iterator(s, " ")); + s << "] " << entry.second << std::endl; + } } void printActionGraph(std::ostream &s) const @@ -186,21 +235,24 @@ class AGG { std::vector projectionTypes; // payoff function for each action node \in S - std::vector payoffs; + std::vector payoffs; + + // exact (arbitrary-precision) counterpart of payoffs, for each action node \in S + std::vector exactPayoffs; // auxiliary data structures // originally: // foreach s \in S, foreach i \in N, foreach s_i \in S_i, // the 'contribution' of s_i to D^(s) - std::vector>> projection; + std::vector>> projection; - // foreach s \in S, foreach i \in N, the projected mixed strat - // which is a prob distribution over the set of 'contributions' - std::vector> projectedStrat; + ConvolutionState m_state; + ConvolutionState m_exactState; - // foreach s in S, i in N, the full set of projected actions. - std::vector> fullProjectedStrat; + // full set of projected actions, filled once from the constructor's projS argument; read by + // gametracer::aggame for jacobian bookkeeping, not used by AGG's own convolution algorithm. + std::vector>> fullProjectedStrat; // foreach s in S, foreach neighbor of s, its projection function std::vector> projFunctions; @@ -209,15 +261,6 @@ class AGG { // in which we apply the DP algorithm std::vector>> Porder; - // when computing the induced distribution via ComputeP(): - // foreach k<= n-1, - // prob. distrib P_k induced by the partial strat profile of agents o_1..o_k - - // when computing the partial distributions for the payoff jacobian: - // foreach j \in N, - // the partial distribution induced by all agents except j. - std::vector Pr; - // foreach s in S, whether s's neighbors are all action nodes std::vector isPure; @@ -225,7 +268,7 @@ class AGG { std::vector> node2Action; // cache of jacobian entries. - trie_map cache; + trie_map cache; // the unique action sets std::vector uniqueActionSets; @@ -244,18 +287,30 @@ class AGG { // input functor struct input { - explicit input(std::istream &i) : in(i) {} - void operator()(aggpayoff::iterator p) { in >> (*p).second; } + input(std::istream &i, ExactPayoffTable &e) : in(i), exact(e) {} + void operator()(PayoffTable::iterator p) + { + std::string word; + in >> word; + const Number num(word); + p->second = static_cast(num); + exact.insert(std::make_pair(p->first, num)); + } std::istream ∈ + ExactPayoffTable &exact; }; // private static methods: - static void makeCOMPLETEpayoff(std::istream &in, aggpayoff &pay) { pay.in_order(input(in)); } - static void makeMAPPINGpayoff(std::istream &in, aggpayoff &pay, int); + static void makeCOMPLETEpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay) + { + pay.in_order(input(in, exactPay)); + } + static void makeMAPPINGpayoff(std::istream &in, PayoffTable &pay, ExactPayoffTable &exactPay, + int); - static void setProjections(std::vector> &projS, - std::vector>> &proj, int N, int S, + static void setProjections(std::vector>> &projS, + std::vector>> &proj, int N, int S, int P, std::vector> &AS, std::vector> &neighb, std::vector &projTypes); @@ -263,23 +318,40 @@ class AGG { static void getAn(std::multiset &dest, std::vector> &neighb, std::vector &projTypes, int S, int Node, std::vector &path); - static void initPorder(std::vector &Po, int i, int N, std::vector &projS); + static void initPorder(std::vector &Po, int i, int N, + std::vector> &projS); - // private methods: - void computeP(int player, int act, int player2 = -1, int act2 = -1); - void doProjection(int Node, const StrategyProfile &s) + // dispatches to the V-specific working state, so computeP/doProjection don't need to branch. + template ConvolutionState &state() { - doProjection(Node, &(const_cast(s)[0])); + if constexpr (std::is_same_v) { + return m_exactState; + } + else { + return m_state; + } + } + template void computeP(int player, int act, int player2 = -1, int act2 = -1); + template void doProjection(int Node, const StrategyProfile &s) + { + doProjection(Node, &(const_cast &>(s)[0])); } - void doProjection(int Node, int player, const StrategyProfile &s) + template void doProjection(int Node, int player, const StrategyProfile &s) { - doProjection(Node, player, &(const_cast(s)[firstAction(player)])); + doProjection(Node, player, &(const_cast &>(s)[firstAction(player)])); } - void doProjection(int Node, AggNumber *s); - void doProjection(int Node, int player, AggNumber *s); + template void doProjection(int Node, V *s); + template void doProjection(int Node, int player, V *s); + + // exactPayoffs is a std::map, not a trie_map, so this does the inner product by hand. + Rational exactInnerProd(int node, const ConfigDistribution &dist) const; + + // dispatch point used by getV/getJ: V=double uses trie_map's own inner_prod, V=Rational uses + // exactInnerProd. + template V payoffInnerProd(int node, const ConfigDistribution &dist); - void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, aggdistrib &dest, - int plClass2 = -1, int act2 = -1); + void getSymConfigProb(int plClass, StrategyProfile &s, int ownPlClass, int act, + ConfigDistribution &dest, int plClass2 = -1, int act2 = -1); }; } // namespace agg diff --git a/src/games/agg/bagg.cc b/src/games/agg/bagg.cc index 12d36de06a..80b07a824b 100644 --- a/src/games/agg/bagg.cc +++ b/src/games/agg/bagg.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include "games/agg/bagg.h" using namespace std; @@ -11,10 +12,11 @@ using namespace std; namespace Gambit::agg { BAGG::BAGG(int N, int S, vector &numTypes, vector &TDist, - vector>> &typeActionSets, vector>> &ta2a, - std::shared_ptr aggPtr) + vector> &exactTDist, vector>> &typeActionSets, + vector>> &ta2a, std::shared_ptr aggPtr) : numPlayers(N), numActionNodes(S), numTypes(numTypes), indepTypeDist(TDist), - typeActionSets(typeActionSets), typeAction2ActionIndex(ta2a), aggPtr(aggPtr) + exactIndepTypeDist(exactTDist), typeActionSets(typeActionSets), typeAction2ActionIndex(ta2a), + aggPtr(aggPtr) { typeOffset = std::vector(numPlayers + 1); typeOffset[0] = 0; @@ -94,13 +96,18 @@ std::shared_ptr BAGG::makeBAGG(istream &in) // input the type distributions stripComment(in); vector TDist; + vector> exactTDist(N); for (int i = 0; i < N; ++i) { TDist.emplace_back(numTypes[i]); for (int j = 0; j < numTypes[i]; ++j) { - in >> TDist[i][j]; + std::string word; + in >> word; if (!in) { throw std::runtime_error("Error in game file: number expected for type distribution"); } + const Number num(word); + TDist[i][j] = static_cast(num); + exactTDist[i].push_back(num); } } @@ -175,31 +182,32 @@ std::shared_ptr BAGG::makeBAGG(istream &in) } const std::shared_ptr aggPtr = AGG::makeAGG(aggss); - return std::make_shared(N, S, numTypes, TDist, typeActionSets, typeAction2ActionIndex, - aggPtr); + return std::make_shared(N, S, numTypes, TDist, exactTDist, typeActionSets, + typeAction2ActionIndex, aggPtr); } -AggNumber BAGG::getMixedPayoff(int player, StrategyProfile &s) +double BAGG::getMixedPayoff(int player, StrategyProfile &s) { - AggNumber res(0); + double res(0); for (int tp = 0; tp < numTypes[player]; ++tp) { res += indepTypeDist[player][tp] * getMixedPayoff(player, tp, s); } return res; } -AggNumber BAGG::getMixedPayoff(int player, int tp, StrategyProfile &s) +template V BAGG::getMixedPayoff(int player, int tp, const StrategyProfile &s) const { - AggNumber res(0); + V res(0); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { - if (s[act + firstAction(player, tp)] > AggNumber(0.0)) { + if (s[act + firstAction(player, tp)] > V(0)) { res += s[act + firstAction(player, tp)] * getV(player, tp, act, s); } } return res; } -void BAGG::getPayoffVector(AggNumberVector &dest, int player, int tp, const StrategyProfile &s) +void BAGG::getPayoffVector(std::vector &dest, int player, int tp, + const StrategyProfile &s) { assert(player >= 0 && player < getNumPlayers() && tp >= 0 && tp < getNumTypes(player)); for (size_t act = 0; act < typeActionSets[player][tp].size(); ++act) { @@ -207,11 +215,12 @@ void BAGG::getPayoffVector(AggNumberVector &dest, int player, int tp, const Stra } } -void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, int tp, - int action) +template +void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player, int tp, + int action) const { for (int i = 0; i < aggPtr->getNumActions(); ++i) { - as[i] = AggNumber(0.0); + as[i] = V(0); } for (int pl = 0; pl < numPlayers; ++pl) { @@ -219,39 +228,64 @@ void BAGG::getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player for (int t = 0; t < numTypes[pl]; ++t) { for (size_t act = 0; act < typeActionSets[pl][t].size(); ++act) { const int aact = typeAction2ActionIndex[pl][t][act]; - as[aact + aggPtr->firstAction(pl)] += indepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + if constexpr (std::is_same_v) { + as[aact + aggPtr->firstAction(pl)] += + static_cast(exactIndepTypeDist[pl][t]) * s[act + firstAction(pl, t)]; + } + else { + as[aact + aggPtr->firstAction(pl)] += + indepTypeDist[pl][t] * s[act + firstAction(pl, t)]; + } } } } else { const int aact = typeAction2ActionIndex[player][tp][action]; - as[aact + aggPtr->firstAction(player)] = 1; + as[aact + aggPtr->firstAction(player)] = V(1); } } } -AggNumber BAGG::getV(int player, int tp, int action, const StrategyProfile &s) + +template V BAGG::getV(int player, int tp, int action, const StrategyProfile &s) const { - StrategyProfile as(aggPtr->getNumActions()); + StrategyProfile as(aggPtr->getNumActions()); getAGGStrat(as, s, player, tp, action); return aggPtr->getV(player, typeAction2ActionIndex[player][tp][action], as); } -AggNumber BAGG::getPurePayoff(int player, int tp, std::vector &ps) +template void BAGG::getAGGStrat(StrategyProfile &as, + const StrategyProfile &s, int player, int tp, + int action) const; +template void BAGG::getAGGStrat(StrategyProfile &as, + const StrategyProfile &s, int player, int tp, + int action) const; +template double BAGG::getV(int player, int tp, int action, + const StrategyProfile &s) const; +template Rational BAGG::getV(int player, int tp, int action, + const StrategyProfile &s) const; +template double BAGG::getMixedPayoff(int player, int tp, + const StrategyProfile &s) const; +template Rational BAGG::getMixedPayoff(int player, int tp, + const StrategyProfile &s) const; + +// payoff of the pure profile ps, via the degenerate mixed profile and the convolution engine +// (mirrors AGG::getPurePayoff). +template V BAGG::getPurePayoff(int player, int tp, const std::vector &ps) const { - StrategyProfile st(strategyOffset[typeOffset[numPlayers]]); - for (int i = 0; i < strategyOffset[typeOffset[numPlayers]]; i++) { - st[i] = (AggNumber)0.0; - } + StrategyProfile s(strategyOffset[typeOffset[numPlayers]]); for (int i = 0; i < typeOffset[numPlayers]; i++) { - st[strategyOffset[i] + ps[i]] = (AggNumber)1.0; + s[strategyOffset[i] + ps[i]] = V(1); } - return getMixedPayoff(player, tp, st); + return getMixedPayoff(player, tp, s); } +template double BAGG::getPurePayoff(int player, int tp, const std::vector &ps) const; +template Rational BAGG::getPurePayoff(int player, int tp, + const std::vector &ps) const; -void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) +void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) { for (int i = 0; i < aggPtr->getNumActionNodes(); ++i) { - as[i] = AggNumber(0.0); + as[i] = double(0.0); } for (int t = 0; t < numTypes[0]; ++t) { for (size_t act = 0; act < typeActionSets[0][t].size(); ++act) { @@ -261,29 +295,29 @@ void BAGG::getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s) } } -AggNumber BAGG::getSymMixedPayoff(StrategyProfile &s) +double BAGG::getSymMixedPayoff(StrategyProfile &s) { - AggNumber res(0); + double res(0); for (int tp = 0; tp < numTypes[0]; ++tp) { res += indepTypeDist[0][tp] * getSymMixedPayoff(tp, s); } return res; } -AggNumber BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, StrategyProfile &s) { - AggNumber res(0); + double res(0); for (size_t act = 0; act < typeActionSets[0][tp].size(); ++act) { - if (s[act + firstAction(0, tp)] > AggNumber(0.0)) { + if (s[act + firstAction(0, tp)] > double(0.0)) { res += s[act + firstAction(0, tp)] * getSymMixedPayoff(tp, act, s); } } return res; } -AggNumber BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) +double BAGG::getSymMixedPayoff(int tp, int act, StrategyProfile &s) { - StrategyProfile as(aggPtr->getNumActionNodes()); + StrategyProfile as(aggPtr->getNumActionNodes()); getSymAGGStrat(as, s); return aggPtr->getSymMixedPayoff(typeActionSets[0][tp][act], as); } @@ -309,7 +343,7 @@ ostream &operator<<(ostream &s, const BAGG &g) // type distrib for (int i = 0; i < g.getNumPlayers(); ++i) { for (int j = 0; j < g.getNumTypes(i); ++j) { - s << g.indepTypeDist[i][j] << " "; + s << g.exactIndepTypeDist[i][j] << " "; } s << endl; } diff --git a/src/games/agg/bagg.h b/src/games/agg/bagg.h index 21a51e54fc..642947dd03 100644 --- a/src/games/agg/bagg.h +++ b/src/games/agg/bagg.h @@ -34,7 +34,7 @@ template class BAGGMixedStrategyProfileRep; namespace agg { -using ProbDist = std::vector; +using ProbDist = std::vector; class BAGG { public: @@ -46,6 +46,7 @@ class BAGG { static std::shared_ptr makeBAGG(std::istream &in); BAGG(int N, int S, std::vector &numTypes, std::vector &TDist, + std::vector> &exactTDist, std::vector>> &typeActionSets, std::vector>> &ta2a, std::shared_ptr aggPtr); @@ -62,32 +63,36 @@ class BAGG { int firstAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t]; } int lastAction(int pl, int t) const { return strategyOffset[typeOffset[pl] + t + 1]; } - AggNumber getMaxPayoff() const { return aggPtr->getMaxPayoff(); } - AggNumber getMinPayoff() const { return aggPtr->getMinPayoff(); } + template V getMaxPayoff() const { return aggPtr->getMaxPayoff(); } + template V getMinPayoff() const { return aggPtr->getMinPayoff(); } - // exp. payoff under mixed strat profile - AggNumber getMixedPayoff(int player, StrategyProfile &s); - // exp payoff for player, conditioned on her receiving type tp. - AggNumber getMixedPayoff(int player, int tp, StrategyProfile &s); + // exp. payoff under mixed strat profile, summed over player's types. No Rational counterpart; + // callers go through the per-type overload below. + double getMixedPayoff(int player, StrategyProfile &s); - void getPayoffVector(AggNumberVector &dest, int player, int tp, const StrategyProfile &s); - AggNumber getV(int player, int tp, int action, const StrategyProfile &s); + // exp payoff for player, conditioned on her receiving type tp, via the convolution algorithm. + template V getMixedPayoff(int player, int tp, const StrategyProfile &s) const; - AggNumber getPurePayoff(int player, int tp, std::vector &s); - AggNumber getPurePayoff(int player, std::vector &s) + void getPayoffVector(std::vector &dest, int player, int tp, + const StrategyProfile &s); + template V getV(int player, int tp, int action, const StrategyProfile &s) const; + + // payoff for player, conditioned on receiving type tp, under the pure profile s. + template V getPurePayoff(int player, int tp, const std::vector &s) const; + double getPurePayoff(int player, std::vector &s) { - AggNumber r = 0; + double r = 0; for (int i = 0; i < numTypes[player]; ++i) { - r += indepTypeDist[player][i] * getPurePayoff(player, i, s); + r += indepTypeDist[player][i] * getPurePayoff(player, i, s); } return r; } - AggNumber getSymMixedPayoff(StrategyProfile &s); + double getSymMixedPayoff(StrategyProfile &s); - AggNumber getSymMixedPayoff(int tp, StrategyProfile &s); + double getSymMixedPayoff(int tp, StrategyProfile &s); - AggNumber getSymMixedPayoff(int tp, int act, StrategyProfile &s); + double getSymMixedPayoff(int tp, int act, StrategyProfile &s); bool isSymmetric() const { return symmetric; } @@ -96,6 +101,7 @@ class BAGG { int numActionNodes; std::vector numTypes; std::vector indepTypeDist; + std::vector> exactIndepTypeDist; // for each player, each of her types, the set of actions std::vector>> typeActionSets; @@ -110,9 +116,10 @@ class BAGG { bool symmetric; - void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, int tp = -1, - int action = -1); - void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); + template + void getAGGStrat(StrategyProfile &as, const StrategyProfile &s, int player = -1, + int tp = -1, int action = -1) const; + void getSymAGGStrat(StrategyProfile &as, const StrategyProfile &s); }; std::ostream &operator<<(std::ostream &s, const BAGG &g); diff --git a/src/games/agg/trie_map.cc b/src/games/agg/trie_map.cc index 779d911741..b6eee4ddb0 100644 --- a/src/games/agg/trie_map.cc +++ b/src/games/agg/trie_map.cc @@ -26,7 +26,10 @@ namespace Gambit::agg { -template class trie_map; -template std::ostream &operator<<(std::ostream &s, const trie_map &t); +template class trie_map; +template std::ostream &operator<<(std::ostream &s, const trie_map &t); + +template class trie_map; +template std::ostream &operator<<(std::ostream &s, const trie_map &t); } // namespace Gambit::agg diff --git a/src/games/agg/trie_map.imp b/src/games/agg/trie_map.imp index acf6cb544b..f28a7d02a3 100644 --- a/src/games/agg/trie_map.imp +++ b/src/games/agg/trie_map.imp @@ -207,7 +207,7 @@ void trie_map::square(trie_map &dest, size_t keylen, std::vector } v.second = (V)((*p1).second * (*p2).second); if (p1 != p2) { - v.second *= 2; + v.second *= (V)2; } dest += v; } @@ -234,7 +234,7 @@ template void trie_map::square(size_t keylen, std::vector } v.second = (V)((*p1).second * (*p2).second); if (p1 != p2) { - v.second *= 2; + v.second *= (V)2; } (*this) += v; } @@ -443,7 +443,7 @@ template void trie_map::div_helper_mul::add(const std::vector throw std::runtime_error(str.str()); } if (ptr->val->second < (V)0) { - ptr->val->second = 0; + ptr->val->second = (V)0; } } diff --git a/src/games/gameagg.cc b/src/games/gameagg.cc index db6bbb0357..25fc2c88a6 100644 --- a/src/games/gameagg.cc +++ b/src/games/gameagg.cc @@ -56,7 +56,7 @@ Rational AGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const for (int i = 1; i <= aggPtr->getNumPlayers(); i++) { s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } - return Rational(aggPtr->getPurePayoff(p_player->GetNumber() - 1, s)); + return aggPtr->getPurePayoff(p_player->GetNumber() - 1, s); } Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -68,7 +68,7 @@ Rational AGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strat s[i - 1] = GetStrategy(m_game->GetPlayer(i))->GetNumber() - 1; } s[player - 1] = p_strategy->GetNumber() - 1; - return Rational(aggPtr->getPurePayoff(player - 1, s)); + return aggPtr->getPurePayoff(player - 1, s); } //======================================================================== @@ -96,40 +96,40 @@ template class AGGMixedStrategyProfileRep : public MixedStrategyProfil template T AGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { if (i + 1 == ps->GetPlayer()->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int &ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template @@ -139,34 +139,34 @@ T AGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1, const auto player1 = ps1->GetPlayer().get(); const auto player2 = ps2->GetPlayer().get(); if (player1 == player2) { - return (T)0; + return T(0); } auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.aggPtr->getNumActions()); + std::vector s(g.aggPtr->getNumActions()); for (int i = 0; i < g.aggPtr->getNumPlayers(); ++i) { if (i + 1 == player1->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps1->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps1->GetNumber() - 1) = T(1); } else if (i + 1 == player2->GetNumber()) { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { - s[g.aggPtr->firstAction(i) + j] = (T)0; + s[g.aggPtr->firstAction(i) + j] = T(0); } - s.at(g.aggPtr->firstAction(i) + ps2->GetNumber() - 1) = (T)1; + s.at(g.aggPtr->firstAction(i) + ps2->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.aggPtr->getNumActions(i); ++j) { const GameStrategy strategy = this->m_support.GetGame()->GetPlayer(i + 1)->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s[g.aggPtr->firstAction(i) + j] = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.aggPtr->getMixedPayoff(pl - 1, s); + return g.aggPtr->getMixedPayoff(pl - 1, s); } template class AGGMixedStrategyProfileRep; diff --git a/src/games/gameagg.h b/src/games/gameagg.h index 4f2a9134ee..1c50c472f5 100644 --- a/src/games/gameagg.h +++ b/src/games/gameagg.h @@ -83,11 +83,11 @@ class GameAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return Rational(aggPtr->getMinPayoff()); } + Rational GetMinPayoff() const override { return aggPtr->getMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return Rational(aggPtr->getMaxPayoff()); } + Rational GetMaxPayoff() const override { return aggPtr->getMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/games/gamebagg.cc b/src/games/gamebagg.cc index b1fd41c9ae..53041a73e1 100644 --- a/src/games/gamebagg.cc +++ b/src/games/gamebagg.cc @@ -57,7 +57,7 @@ Rational BAGGPureStrategyProfileRep::GetPayoff(const GamePlayer &p_player) const } const int bp = dynamic_cast(*m_game).agent2baggPlayer[p_player->GetNumber()]; const int tp = p_player->GetNumber() - 1 - baggPtr->typeOffset[bp - 1]; - return Rational(baggPtr->getPurePayoff(bp - 1, tp, s)); + return baggPtr->getPurePayoff(bp - 1, tp, s); } Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_strategy) const @@ -71,7 +71,7 @@ Rational BAGGPureStrategyProfileRep::GetStrategyValue(const GameStrategy &p_stra s[player - 1] = p_strategy->GetNumber() - 1; const int bp = dynamic_cast(*m_game).agent2baggPlayer[player]; const int tp = player - 1 - baggPtr->typeOffset[bp - 1]; - return Rational(baggPtr->getPurePayoff(bp - 1, tp, s)); + return baggPtr->getPurePayoff(bp - 1, tp, s); } //======================================================================== @@ -99,7 +99,7 @@ template class BAGGMixedStrategyProfileRep : public MixedStrategyProfi template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); const auto ns = g.GetStrategies().shape(); int bplayer = -1, btype = -1; for (int i = 0, offs = 0; i < g.baggPtr->getNumPlayers(); ++i) { @@ -113,18 +113,18 @@ template T BAGGMixedStrategyProfileRep::GetPayoff(int pl) const ->GetPlayer(g.baggPtr->typeOffset[i] + tp + 1) ->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); - s.at(offs) = (ind == -1) ? (T)0 : this->m_probs.GetFlattened()[ind]; + s.at(offs) = (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) const { auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -134,9 +134,9 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } if (g.baggPtr->typeOffset[i] + tp + 1 == ps->GetPlayer()->GetNumber()) { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps->GetNumber() - 1) = T(1); } else { for (int j = 0; j < g.baggPtr->getNumActions(i, tp); ++j) { @@ -150,7 +150,7 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps) } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template @@ -160,11 +160,11 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 const auto player1 = ps1->GetPlayer().get(); const auto player2 = ps2->GetPlayer().get(); if (player1 == player2) { - return (T)0; + return T(0); } auto &g = dynamic_cast(*(this->m_support.GetGame())); - std::vector s(g.GetStrategies().size()); + std::vector s(g.GetStrategies().size()); int bplayer = -1, btype = -1; for (int i = 0; i < g.baggPtr->getNumPlayers(); ++i) { for (int tp = 0; tp < g.baggPtr->getNumTypes(i); ++tp) { @@ -175,15 +175,15 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 if (g.baggPtr->typeOffset[i] + tp + 1 == player1->GetNumber()) { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps1->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps1->GetNumber() - 1) = T(1); } else if (g.baggPtr->typeOffset[i] + tp + 1 == player2->GetNumber()) { for (int j = 0; j < g.baggPtr->getNumActions(i, tp); ++j) { - s.at(g.baggPtr->firstAction(i, tp) + j) = 0; + s.at(g.baggPtr->firstAction(i, tp) + j) = T(0); } - s.at(g.baggPtr->firstAction(i, tp) + ps2->GetNumber() - 1) = 1; + s.at(g.baggPtr->firstAction(i, tp) + ps2->GetNumber() - 1) = T(1); } else { for (unsigned int j = 0; j < g.baggPtr->typeActionSets.at(i).at(tp).size(); ++j) { @@ -192,12 +192,12 @@ T BAGGMixedStrategyProfileRep::GetPayoffDeriv(int pl, const GameStrategy &ps1 ->GetStrategy(j + 1); const int ind = this->m_profileIndex.at(strategy); s.at(g.baggPtr->firstAction(i, tp) + j) = - static_cast((ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]); + (ind == -1) ? T(0) : this->m_probs.GetFlattened()[ind]; } } } } - return (T)g.baggPtr->getMixedPayoff(bplayer, btype, s); + return g.baggPtr->getMixedPayoff(bplayer, btype, s); } template class BAGGMixedStrategyProfileRep; diff --git a/src/games/gamebagg.h b/src/games/gamebagg.h index e554a64956..319941e927 100644 --- a/src/games/gamebagg.h +++ b/src/games/gamebagg.h @@ -90,11 +90,11 @@ class GameBAGGRep : public GameRep { bool IsPerfectRecall() const override { return true; } bool IsConstSum() const override; /// Returns the smallest payoff to any player in any outcome of the game - Rational GetMinPayoff() const override { return Rational(baggPtr->getMinPayoff()); } + Rational GetMinPayoff() const override { return baggPtr->getMinPayoff(); } /// Returns the smallest payoff to the player in any outcome of the game Rational GetPlayerMinPayoff(const GamePlayer &) const override; /// Returns the largest payoff to any player in any outcome of the game - Rational GetMaxPayoff() const override { return Rational(baggPtr->getMaxPayoff()); } + Rational GetMaxPayoff() const override { return baggPtr->getMaxPayoff(); } /// Returns the largest payoff to the player in any outcome of the game Rational GetPlayerMaxPayoff(const GamePlayer &) const override; //@} diff --git a/src/games/number.h b/src/games/number.h index 9c3f4963c2..9e508444c1 100644 --- a/src/games/number.h +++ b/src/games/number.h @@ -23,6 +23,11 @@ #ifndef LIBGAMBIT_NUMBER_H #define LIBGAMBIT_NUMBER_H +#include +#include +#include "core/util.h" +#include "core/rational.h" + namespace Gambit { /// This class represents a numerical data value in a game. @@ -84,6 +89,8 @@ class Number { explicit operator const double &() const { return m_double; } explicit operator const Rational &() const { return m_rational; } explicit operator const std::string &() const { return m_text; } + + friend std::ostream &operator<<(std::ostream &s, const Number &n) { return s << n.m_text; } }; } // namespace Gambit diff --git a/src/solvers/gtracer/aggame.cc b/src/solvers/gtracer/aggame.cc index 6d9da55457..0ef1f43a14 100644 --- a/src/solvers/gtracer/aggame.cc +++ b/src/solvers/gtracer/aggame.cc @@ -32,29 +32,29 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t const int numNei = aggPtr->neighbors[Node].size(); // assert(aggPtr->isPure[Node]||tasks.size()==0); - std::vector strat(numNei); - agg::AGG::config a(numNei, 0); + std::vector strat(numNei); + agg::AGG::Config a(numNei, 0); // compute the full distrib - aggPtr->computeP(player1, act1); + aggPtr->computeP(player1, act1); // store the full distrib in Pr[player1] - aggPtr->Pr[player1].swap(aggPtr->Pr[numPlayers - 1]); + aggPtr->m_state.Pr[player1].swap(aggPtr->m_state.Pr[numPlayers - 1]); for (i = 0; i < (int)tasks.size(); i++) { // assert(tasks[i]!=player1); - agg::aggdistrib &P = aggPtr->Pr[tasks[i]]; + agg::ConfigDistribution &P = aggPtr->m_state.Pr[tasks[i]]; // P.clear(); // to get ready for division, we need clear() - P = aggPtr->Pr[player1]; + P = aggPtr->m_state.Pr[player1]; bool NullOnly = true; for (j = 0; j < numNei; ++j) { a[j]++; - auto pp = aggPtr->projectedStrat[Node][tasks[i]].find(a); - if (pp == aggPtr->projectedStrat[Node][tasks[i]].end()) { + auto pp = aggPtr->m_state.projectedStrat[Node][tasks[i]].find(a); + if (pp == aggPtr->m_state.projectedStrat[Node][tasks[i]].end()) { strat[j] = 0; } else { strat[j] = pp->second; - if (strat[j] > (agg::AggNumber)0) { + if (strat[j] > (double)0) { NullOnly = false; } } @@ -65,7 +65,7 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t cout << "dividing " << endl; P.print_in_order(); cout << endl << "by ["; - copy(strat.begin(), strat.end(), ostream_iterator(cout, " ")); + copy(strat.begin(), strat.end(), ostream_iterator(cout, " ")); cout << "]\n"; #endif if (!NullOnly) { @@ -79,7 +79,8 @@ void aggame::computePartialP_PureNode(int player1, int act1, std::vector &t } void aggame::computePartialP_bisect(int player1, int act1, std::vector::iterator start, - std::vector::iterator endp, agg::aggdistrib &temp) const + std::vector::iterator endp, + agg::ConfigDistribution &temp) const { #ifdef AGGDEBUG cout << "calling computePartialP_bisect with player1=" << player1 << ", act1=" << act1 @@ -87,7 +88,7 @@ void aggame::computePartialP_bisect(int player1, int act1, std::vector::ite << endl; #endif if (endp - start == 1) { - aggPtr->Pr[*start].reset(); + aggPtr->m_state.Pr[*start].reset(); return; } const int Node = aggPtr->actionSets[player1][act1]; @@ -102,42 +103,44 @@ void aggame::computePartialP_bisect(int player1, int act1, std::vector::ite computePartialP_bisect(player1, act1, mid, endp, temp); temp.reset(); - temp = aggPtr->projectedStrat[Node][*start]; + temp = aggPtr->m_state.projectedStrat[Node][*start]; if (mid - start > 1) { - temp.multiply(aggPtr->Pr[*start], numNei, aggPtr->projFunctions[Node]); + temp.multiply(aggPtr->m_state.Pr[*start], numNei, aggPtr->projFunctions[Node]); } if (mid - start == 1) { - // assert(aggPtr->Pr[*start].empty()); - aggPtr->Pr[*start] = aggPtr->projectedStrat[Node][*mid]; + // assert(aggPtr->m_state.Pr[*start].empty()); + aggPtr->m_state.Pr[*start] = aggPtr->m_state.projectedStrat[Node][*mid]; if (endp - mid > 1) { - aggPtr->Pr[*start].multiply(aggPtr->Pr[*mid], numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[*start].multiply(aggPtr->m_state.Pr[*mid], numNei, + aggPtr->projFunctions[Node]); } } else { for (ptr = start; ptr != mid; ++ptr) { player2 = *ptr; - aggPtr->Pr[player2].multiply(aggPtr->projectedStrat[Node][*mid], numNei, - aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(aggPtr->m_state.projectedStrat[Node][*mid], numNei, + aggPtr->projFunctions[Node]); if (endp - mid > 1) { - aggPtr->Pr[player2].multiply(aggPtr->Pr[*mid], numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(aggPtr->m_state.Pr[*mid], numNei, + aggPtr->projFunctions[Node]); } } } if (endp - mid == 1) { - // assert(aggPtr->Pr[*mid].empty()); - aggPtr->Pr[*mid] = temp; + // assert(aggPtr->m_state.Pr[*mid].empty()); + aggPtr->m_state.Pr[*mid] = temp; } else { for (ptr = mid; ptr != endp; ++ptr) { player2 = *ptr; - aggPtr->Pr[player2].multiply(temp, numNei, aggPtr->projFunctions[Node]); + aggPtr->m_state.Pr[player2].multiply(temp, numNei, aggPtr->projFunctions[Node]); } } } -void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) const +void aggame::payoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const { // compute jacobian // s: mixed strat @@ -145,7 +148,7 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) #ifdef AGGDEBUG cout << "calling payoffMatrix with stratety s=" << endl << s << endl; #endif - agg::AggNumber fuzzcount; + double fuzzcount; int rown, coln, rowi, coli, act1, act2, currNode, numNei; static std::vector::iterator p; static std::vector tasks, spares, nontasks; @@ -243,40 +246,41 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) computePartialP_PureNode(rown, act1, tasks); } else { // do bisection - computePartialP_bisect(rown, act1, tasks.begin(), tasks.end(), aggPtr->Pr[rown]); + computePartialP_bisect(rown, act1, tasks.begin(), tasks.end(), aggPtr->m_state.Pr[rown]); #ifdef AGGDEBUG cout << "after calling computePartialP_bisect:" << endl; for (int tt = 0; tt < tasks.size(); tt++) { cout << "for player " << tasks[tt] << endl; - cout << aggPtr->Pr[tasks[tt]] << endl; + cout << aggPtr->m_state.Pr[tasks[tt]] << endl; } #endif // now apply rown's action (act1), and the strategies of // players in nontasks - aggPtr->Pr[rown].reset(); - aggPtr->Pr[rown].insert(make_pair(aggPtr->projection[currNode][rown][act1], 1.0)); + aggPtr->m_state.Pr[rown].reset(); + aggPtr->m_state.Pr[rown].insert(make_pair(aggPtr->projection[currNode][rown][act1], 1.0)); for (p = nontasks.begin(); p != nontasks.end(); ++p) { - aggPtr->Pr[rown].multiply(aggPtr->projectedStrat[currNode][*p], numNei, - aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[rown].multiply(aggPtr->m_state.projectedStrat[currNode][*p], numNei, + aggPtr->projFunctions[currNode]); } #ifdef AGGDEBUG cout << "the polynomial product of strats of player " << rown << " and players in the vector nontasks is:" << endl; - cout << aggPtr->Pr[rown] << endl; + cout << aggPtr->m_state.Pr[rown] << endl; #endif if (tasks.size() == 1) { - aggPtr->Pr[tasks[0]] = aggPtr->Pr[rown]; + aggPtr->m_state.Pr[tasks[0]] = aggPtr->m_state.Pr[rown]; } else { for (p = tasks.begin(); p != tasks.end(); ++p) { - if (aggPtr->Pr[*p].empty()) { + if (aggPtr->m_state.Pr[*p].empty()) { std::cerr << "AGG::payoffMatrix() ERROR for rown=" << rown << " act1=" << act1 << " *p=" << *p << ": the distribution should not be empty!" << std::endl; #ifdef AGGDEBUG std::cerr << "strategy profile is: " << s << std::endl; #endif } - aggPtr->Pr[*p].multiply(aggPtr->Pr[rown], numNei, aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[*p].multiply(aggPtr->m_state.Pr[rown], numNei, + aggPtr->projFunctions[currNode]); } // end for(p=tasks.begin... } @@ -285,22 +289,22 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) // we store this distrib in Pr[rown][act1][rown] if (!spares.empty()) { // assert(tasks.size()>0); - aggPtr->Pr[rown].reset(); - aggPtr->Pr[rown].multiply(aggPtr->Pr[tasks[0]], - aggPtr->projectedStrat[currNode][tasks[0]], numNei, - aggPtr->projFunctions[currNode]); + aggPtr->m_state.Pr[rown].reset(); + aggPtr->m_state.Pr[rown].multiply(aggPtr->m_state.Pr[tasks[0]], + aggPtr->m_state.projectedStrat[currNode][tasks[0]], + numNei, aggPtr->projFunctions[currNode]); } } // end else #ifdef AGGDEBUG cout << "after computing parital distributions, the distributions are" << endl; for (int tt = 0; tt < numPlayers; tt++) { cout << "for player " << tt << endl; - cout << aggPtr->Pr[tt]; + cout << aggPtr->m_state.Pr[tt]; cout << endl; } #endif // compute entries - agg::AggNumber undisturbedPayoff; + double undisturbedPayoff; bool hasUndisturbed = false; if (!spares.empty()) { // for players in spares, we compute one undisturbed payoff @@ -314,8 +318,8 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) for (p = tasks.begin(); p != tasks.end(); ++p) { for (act2 = 0; act2 < aggPtr->actions[*p]; act2++) { // act2: col action - if (aggPtr->projectedStrat[currNode][*p].size() == 1 && - aggPtr->projectedStrat[currNode][*p].begin()->first == + if (aggPtr->m_state.projectedStrat[currNode][*p].size() == 1 && + aggPtr->m_state.projectedStrat[currNode][*p].begin()->first == aggPtr->projection[currNode][*p][act2]) { computeUndisturbedPayoff(undisturbedPayoff, hasUndisturbed, rown, act1, *p); savePayoff(dest, rown, act1, *p, act2, undisturbedPayoff, aggPtr->cache); @@ -327,8 +331,8 @@ void aggame::payoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) } // end for(rown } -void aggame::computeUndisturbedPayoff(agg::AggNumber &undisturbedPayoff, bool &has, int player1, - int act1, int player2) const +void aggame::computeUndisturbedPayoff(double &undisturbedPayoff, bool &has, int player1, int act1, + int player2) const { if (has) { return; @@ -336,28 +340,26 @@ void aggame::computeUndisturbedPayoff(agg::AggNumber &undisturbedPayoff, bool &h const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); if (player2 == player1) { - undisturbedPayoff = aggPtr->Pr[player2].inner_prod(aggPtr->payoffs[Node]); + undisturbedPayoff = aggPtr->m_state.Pr[player2].inner_prod(aggPtr->payoffs[Node]); } else { - // assert(aggPtr->projectedStrat[Node][player2].size()==1); - undisturbedPayoff = - aggPtr->Pr[player2].inner_prod(aggPtr->projectedStrat[Node][player2].begin()->first, - numNei, aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); + // assert(aggPtr->m_state.projectedStrat[Node][player2].size()==1); + undisturbedPayoff = aggPtr->m_state.Pr[player2].inner_prod( + aggPtr->m_state.projectedStrat[Node][player2].begin()->first, numNei, + aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); } has = true; } -void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - agg::AggNumber result, agg::trie_map &cache, - bool partial) const +void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, double result, + agg::trie_map &cache, bool partial) const { const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); if (!partial) { - std::pair, agg::AggNumber> pair1(aggPtr->projection[Node][player2][act2], - result); + std::pair, double> pair1(aggPtr->projection[Node][player2][act2], result); pair1.first.reserve(numNei + 3); pair1.first.push_back(player1); pair1.first.push_back(act1); @@ -367,8 +369,7 @@ void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int a if (aggPtr->node2Action[Node][player2] != -1 && aggPtr->fullProjectedStrat[Node][player1].count(aggPtr->projection[Node][player2][act2])) { - std::pair, agg::AggNumber> pair2(aggPtr->projection[Node][player2][act2], - result); + std::pair, double> pair2(aggPtr->projection[Node][player2][act2], result); pair2.first.reserve(numNei + 3); pair2.first.push_back(player2); pair2.first.push_back(aggPtr->node2Action[Node][player2]); @@ -379,29 +380,29 @@ void aggame::savePayoff(cmatrix &dest, int player1, int act1, int player2, int a } void aggame::computePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - agg::trie_map &cache) const + agg::trie_map &cache) const { const int Node = aggPtr->actionSets[player1][act1]; const int numNei = aggPtr->neighbors[Node].size(); - std::pair, agg::AggNumber> insPair(aggPtr->projection[Node][player2][act2], 0); + std::pair, double> insPair(aggPtr->projection[Node][player2][act2], 0); insPair.first.reserve(numNei + 3); insPair.first.push_back(player1); insPair.first.push_back(act1); insPair.first.push_back(player2); - const std::pair::iterator, bool> r = cache.insert(insPair); + const std::pair::iterator, bool> r = cache.insert(insPair); if (!r.second) { dest(act1 + firstAction(player1), act2 + firstAction(player2)) = r.first->second; } else { r.first->second = - aggPtr->Pr[player2].inner_prod(aggPtr->projection[Node][player2][act2], numNei, - aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); + aggPtr->m_state.Pr[player2].inner_prod(aggPtr->projection[Node][player2][act2], numNei, + aggPtr->projFunctions[Node], aggPtr->payoffs[Node]); savePayoff(dest, player1, act1, player2, act2, r.first->second, cache, r.second); } } -void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fuzz) const +void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, double fuzz) const { const std::vector sp(s.values(), s.values() + s.getm()); // simple implementation using expected payoffs: @@ -415,14 +416,13 @@ void aggame::KSymPayoffMatrix(cmatrix &dest, const cvector &s, agg::AggNumber fu for (int cola = 0; cola < getNumKSymActions(colcls); ++cola) { dest(rowa + firstKSymAction(rowcls), cola + firstKSymAction(colcls)) = - (agg::AggNumber)multiplier * - aggPtr->getKSymMixedPayoff(sp, rowcls, rowa, colcls, cola); + (double)multiplier * aggPtr->getKSymMixedPayoff(sp, rowcls, rowa, colcls, cola); } } } } - agg::AggNumber fuzzcount; + double fuzzcount; for (int rown = 0; rown < getNumPlayerClasses(); ++rown) { fuzzcount = fuzz; for (int rowi = firstKSymAction(rown); rowi < lastKSymAction(rown); rowi++) { diff --git a/src/solvers/gtracer/aggame.h b/src/solvers/gtracer/aggame.h index ee9731d1ae..d1b4b8346d 100644 --- a/src/solvers/gtracer/aggame.h +++ b/src/solvers/gtracer/aggame.h @@ -44,8 +44,8 @@ class aggame : public gnmgame { double getMixedPayoff(int player, const cvector &s) const override { - std::vector sp(s.values(), s.values() + s.getm()); - return (double)aggPtr->getMixedPayoff(player, sp); + const std::vector sp(s.values(), s.values() + s.getm()); + return aggPtr->getMixedPayoff(player, sp); } double getKSymMixedPayoff(int cls, cvector &s) @@ -73,7 +73,7 @@ class aggame : public gnmgame { } double getPurePayoff(int player, const std::vector &s) const override { - return aggPtr->getPurePayoff(player, s); + return aggPtr->getPurePayoff(player, s); } void setPurePayoff(int player, const std::vector &s, double value) override @@ -93,15 +93,14 @@ class aggame : public gnmgame { // helper functions for computing jacobian void computePartialP_PureNode(int player, int act, std::vector &tasks) const; void computePartialP_bisect(int player, int act, std::vector::iterator f, - std::vector::iterator l, Gambit::agg::aggdistrib &temp) const; + std::vector::iterator l, + Gambit::agg::ConfigDistribution &temp) const; void computePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - Gambit::agg::trie_map &cache) const; - void savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, - Gambit::agg::AggNumber result, - Gambit::agg::trie_map &cache, - bool partial = false) const; - void computeUndisturbedPayoff(Gambit::agg::AggNumber &undisturbedPayoff, bool &has, int player1, - int act1, int player2) const; + Gambit::agg::trie_map &cache) const; + void savePayoff(cmatrix &dest, int player1, int act1, int player2, int act2, double result, + Gambit::agg::trie_map &cache, bool partial = false) const; + void computeUndisturbedPayoff(double &undisturbedPayoff, bool &has, int player1, int act1, + int player2) const; }; } // end namespace Gambit::gametracer diff --git a/tests/test_agg.py b/tests/test_agg.py index 1f6b071fbd..c417a6f3af 100644 --- a/tests/test_agg.py +++ b/tests/test_agg.py @@ -1,8 +1,12 @@ """Tests of generic Game operations on action graph game (AGG/BAGG) representations. """ +import itertools + import pytest +import pygambit as gbt + from . import games AGG_BAGG_GAME_PATHS = [ @@ -38,3 +42,134 @@ def test_agg_bagg_to_nfg(game_path): game = games.read_from_file(game_path) serialized_game = game.to_nfg() assert serialized_game[:3] == "NFG" + + +def test_agg_fraction_and_long_decimal_payoffs_parsed_exactly(): + """Payoffs written as a fraction ("1/3") or a decimal too precise for a double to round-trip + are parsed into exact Rationals, not silently rounded through a bare double read. + + 2x2_fraction_payoffs.agg is 2x2.agg with two of its four payoffs replaced: 35 -> 1/3 and + 95 -> 0.123456789012345. + """ + game = games.read_from_file("2x2_fraction_payoffs.agg") + assert game.max_payoff == gbt.Rational(1, 3) + assert game.min_payoff == -10 + for player in game.players: + assert player.max_payoff == gbt.Rational(1, 3) + assert player.min_payoff == -10 + + # pure-strategy payoff lookup also reports the exact value, not a rounded approximation + profile = game.mixed_strategy_profile(rational=True) + p0, p1 = game.players + s0 = list(p0.strategies) + s1 = list(p1.strategies) + profile[s0[0]] = gbt.Rational(1) + profile[s0[1]] = gbt.Rational(0) + profile[s1[0]] = gbt.Rational(0) + profile[s1[1]] = gbt.Rational(1) + assert profile.payoff(p0) == gbt.Rational(1, 3) + + +def test_bagg_fraction_type_distribution_parsed_exactly(): + """BAGG type-distribution probabilities written as a fraction ("1/3") are parsed exactly, + and the exact payoff computation over them agrees with the double engine on the same + profile. + + 2x2_fraction_types.bagg: player 1 has two types (weights 1/3, 2/3), player 2 has one. + """ + game = games.read_from_file("2x2_fraction_types.bagg") + p1t0, p1t1, p2 = game.players + exact = game.mixed_strategy_profile(rational=True) + dbl = game.mixed_strategy_profile(rational=False) + for profile, one, zero in [(exact, gbt.Rational(1), gbt.Rational(0)), (dbl, 1.0, 0.0)]: + s0, s1, s2 = list(p1t0.strategies), list(p1t1.strategies), list(p2.strategies) + profile[s0[0]], profile[s0[1]] = one, zero + profile[s1[0]], profile[s1[1]] = zero, one + profile[s2[0]], profile[s2[1]] = one, zero + assert exact.payoff(p2) == gbt.Rational(22) + assert float(exact.payoff(p2)) == dbl.payoff(p2) + + +@pytest.mark.parametrize("game_path", ["2x2.agg", "2x2.bagg"]) +def test_agg_bagg_mixed_strategy_profile_rational_exact_payoff(game_path): + """AGG/BAGG mixed-strategy payoffs support exact (rational) computation, not just double. + + 2x2.agg has a known mixed equilibrium at (10/11, 1/11) for both players, with payoff + exactly -5/11 to each -- computed here directly (not via a solver), confirming zero regret. + """ + game = games.read_from_file(game_path) + profile = game.mixed_strategy_profile(rational=True) + for player in game.players: + strategies = list(player.strategies) + profile[strategies[0]] = gbt.Rational(10, 11) + profile[strategies[1]] = gbt.Rational(1, 11) + for player in game.players: + assert profile.payoff(player) == gbt.Rational(-5, 11) + assert profile.max_regret() == 0 + + +@pytest.mark.parametrize("game_path", ["2x2.agg", "2x2.bagg"]) +def test_agg_bagg_rational_algorithms_find_exact_mixed_equilibrium(game_path): + """lcp_solve(rational=True) and enummixed_solve(rational=True) find the (10/11, 1/11) + mixed equilibrium of 2x2.agg exactly. See test_nash.py::test_lcp_strategy_double_12 for the + equivalent full-precision check via the standard solver-test harness. + """ + game = games.read_from_file(game_path) + results = [ + gbt.nash.lcp_solve(game, rational=True, use_strategic=True), + gbt.nash.enummixed_solve(game, rational=True), + ] + for result in results: + mixed = [eq for eq in result.equilibria if any(0 < eq[s] < 1 for s in game.strategies)] + assert len(mixed) == 1 + for player in game.players: + for strategy in player.strategies: + assert mixed[0][strategy] in (gbt.Rational(10, 11), gbt.Rational(1, 11)) + assert mixed[0].max_regret() == 0 + + +def _set_pure_profile(profile, players, contingency): + for player, strat_index in zip(players, contingency, strict=True): + for i, strategy in enumerate(player.strategies): + profile[strategy] = gbt.Rational(1) if i == strat_index else gbt.Rational(0) + + +@pytest.mark.parametrize("game_path", ["2x2.bagg", "2x2_fraction_types.bagg"]) +def test_bagg_pure_strategy_payoff_matches_degenerate_mixed_profile(game_path): + """BAGG's exact pure-strategy payoff agrees, for every pure-strategy contingency, with the + payoff of the corresponding degenerate mixed profile computed via the general convolution + engine -- covering agg::BAGG::getPurePayoff, which had no dedicated test before. + """ + game = games.read_from_file(game_path) + players = list(game.players) + for contingency in itertools.product(*(range(len(list(p.strategies))) for p in players)): + pure_payoffs = [game[contingency][p] for p in players] + + profile = game.mixed_strategy_profile(rational=True) + _set_pure_profile(profile, players, contingency) + mixed_payoffs = [profile.payoff(p) for p in players] + + assert pure_payoffs == mixed_payoffs + + +def test_bagg_pure_strategy_payoff_with_multiple_players_and_types(): + """Same cross-check as above, sampled (full enumeration is 7**6 contingencies) on a BAGG + with several players each holding several types, to exercise getPurePayoff's + handling of more than one other player's type distribution at once. + """ + game = games.read_from_file("Bayesian-Coffee-3-2-2-3.bagg") + players = list(game.players) + sizes = [len(list(p.strategies)) for p in players] + contingencies = [ + tuple(0 for _ in sizes), + tuple(size - 1 for size in sizes), + tuple(i % size for i, size in enumerate(sizes)), + ] + for contingency in contingencies: + pure_payoffs = [game[contingency][p] for p in players] + + profile = game.mixed_strategy_profile(rational=True) + _set_pure_profile(profile, players, contingency) + mixed_payoffs = [profile.payoff(p) for p in players] + + assert pure_payoffs == mixed_payoffs diff --git a/tests/test_games/2x2_fraction_payoffs.agg b/tests/test_games/2x2_fraction_payoffs.agg new file mode 100644 index 0000000000..b2324eff9e --- /dev/null +++ b/tests/test_games/2x2_fraction_payoffs.agg @@ -0,0 +1,33 @@ +#AGG +#number of players: +2 +#number of action nodes: +2 +#number of func nodes: +0 + +#sizes of action sets: +2 2 + +#action sets: +0 1 +0 1 + + +#the action graph: +2 0 1 +2 1 0 + +#the types of func nodes: +#0: sum +#1: existence +#2: highest +#3: lowest + + +#the payoffs: +#now the payoff values: one row per action node. +#For each row: first, the type of the payoff format +#Then payoffs are given in lexicographical order of the input configurations +0 1/3 -4 +0 -10 0.123456789012345 diff --git a/tests/test_games/2x2_fraction_types.bagg b/tests/test_games/2x2_fraction_types.bagg new file mode 100644 index 0000000000..630b583ea1 --- /dev/null +++ b/tests/test_games/2x2_fraction_types.bagg @@ -0,0 +1,21 @@ +#BAGG +# Player 1 has two types with fractional probabilities (1/3, 2/3); player 2 has one type. +# Wraps the same 2x2 payoff structure as 2x2.agg, used to test that BAGG's type-distribution +# parsing (and the exact weighted-sum computation over it) handles fraction literals exactly. +2 +2 +0 +2 1 +1/3 2/3 +1 +2 +2 +2 +0 1 +0 1 +0 1 +2 0 1 +2 1 0 + +0 35 -4 +0 -10 95 diff --git a/tests/test_nash.py b/tests/test_nash.py index 3a213beafb..8e806dac2d 100644 --- a/tests/test_nash.py +++ b/tests/test_nash.py @@ -1368,7 +1368,7 @@ class QREquilibriumTestCase: marks=pytest.mark.nash_lcp_strategy, id="test_lcp_strategy_double_11", ), - # Action graph game + # Action graph game, solved exactly (Rational arithmetic throughout). pytest.param( EquilibriumTestCase( factory=functools.partial(games.read_from_file, "2x2.agg"),