herta
Version:
Advanced mathematics framework for scientific, engineering, and financial applications
683 lines (572 loc) • 20.9 kB
JavaScript
/**
* Game Theory module for herta.js
* Provides tools for game-theoretic analysis and equilibrium finding
*/
const arithmetic = require('../core/arithmetic');
const matrix = require('../core/matrix');
const gameTheory = {};
/**
* Find the Nash equilibrium for a 2-player zero-sum game using minimax
* @param {Array} payoffMatrix - Payoff matrix for player 1 (player 2 gets negative)
* @returns {Object} - Nash equilibrium strategies and value of the game
*/
gameTheory.zeroSumNashEquilibrium = function (payoffMatrix) {
const rows = payoffMatrix.length;
const cols = payoffMatrix[0].length;
// Find the maximin strategy for player 1
const rowMinValues = payoffMatrix.map((row) => Math.min(...row));
const maximinValue = Math.max(...rowMinValues);
const maximinIndices = rowMinValues
.map((value, index) => (value === maximinValue ? index : -1))
.filter((index) => index !== -1);
// Find the minimax strategy for player 2
const columnMaxValues = Array(cols).fill(0);
for (let j = 0; j < cols; j++) {
let colMax = -Infinity;
for (let i = 0; i < rows; i++) {
colMax = Math.max(colMax, payoffMatrix[i][j]);
}
columnMaxValues[j] = colMax;
}
const minimaxValue = Math.min(...columnMaxValues);
const minimaxIndices = columnMaxValues
.map((value, index) => (value === minimaxValue ? index : -1))
.filter((index) => index !== -1);
// Check if there's a pure strategy Nash equilibrium (saddle point)
if (maximinValue === minimaxValue) {
// Find the saddle point(s)
const saddlePoints = [];
for (const i of maximinIndices) {
for (const j of minimaxIndices) {
if (payoffMatrix[i][j] === maximinValue) {
saddlePoints.push([i, j]);
}
}
}
if (saddlePoints.length > 0) {
return {
type: 'pure',
equilibria: saddlePoints,
value: maximinValue,
player1Strategy: saddlePoints.map((point) => point[0]),
player2Strategy: saddlePoints.map((point) => point[1])
};
}
}
// If no pure strategy equilibrium, solve for mixed strategy
// Use linear programming to find the mixed Nash equilibrium
try {
return this._solveZeroSumMixed(payoffMatrix);
} catch (e) {
return {
type: 'unknown',
error: 'Could not determine mixed strategy equilibrium',
message: e.message
};
}
};
/**
* Solve for mixed strategy Nash equilibrium in a zero-sum game
* @private
* @param {Array} payoffMatrix - Payoff matrix
* @returns {Object} - Mixed strategy Nash equilibrium
*/
gameTheory._solveZeroSumMixed = function (payoffMatrix) {
const rows = payoffMatrix.length;
const cols = payoffMatrix[0].length;
// Implement a simplified version of the simplex algorithm for zero-sum games
// Note: For general linear programming, use optimization.linearProgramming
// Step 1: Ensure all payoffs are positive
// (add a constant if necessary)
let minValue = Infinity;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
minValue = Math.min(minValue, payoffMatrix[i][j]);
}
}
const adjustedMatrix = JSON.parse(JSON.stringify(payoffMatrix));
const adjustment = minValue < 0 ? -minValue : 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
adjustedMatrix[i][j] += adjustment;
}
}
// Step 2: Solve for Player 2's strategy using linear programming
// min z = x₁ + x₂ + ... + xₙ
// subject to:
// A₁₁x₁ + A₂₁x₂ + ... + Aₙ₁xₙ ≥ 1
// ...
// A₁ₘx₁ + A₂ₘx₂ + ... + Aₙₘxₙ ≥ 1
// x₁, x₂, ..., xₙ ≥ 0
// Simplified solution using approximation
let p2Strategy = Array(cols).fill(1 / cols); // Start with uniform distribution
const iterations = 1000;
const learningRate = 0.01;
for (let iter = 0; iter < iterations; iter++) {
// Calculate expected payoff for Player 1's pure strategies
const expectedPayoffs = [];
for (let i = 0; i < rows; i++) {
let payoff = 0;
for (let j = 0; j < cols; j++) {
payoff += adjustedMatrix[i][j] * p2Strategy[j];
}
expectedPayoffs.push(payoff);
}
// Find Player 1's best response
const maxPayoff = Math.max(...expectedPayoffs);
const bestResponses = expectedPayoffs
.map((value, index) => (value === maxPayoff ? index : -1))
.filter((index) => index !== -1);
// Update Player 2's strategy using gradient descent
for (let j = 0; j < cols; j++) {
let gradient = 0;
for (const i of bestResponses) {
gradient += adjustedMatrix[i][j] / bestResponses.length;
}
p2Strategy[j] = Math.max(0, p2Strategy[j] - learningRate * gradient);
}
// Normalize strategy
const sum = p2Strategy.reduce((acc, val) => acc + val, 0);
if (sum > 0) {
p2Strategy = p2Strategy.map((val) => val / sum);
} else {
p2Strategy = Array(cols).fill(1 / cols);
}
}
// Step 3: Calculate the value of the game and Player 1's strategy
// Calculate expected payoff matrix
const expectedPayoffs = [];
for (let i = 0; i < rows; i++) {
let payoff = 0;
for (let j = 0; j < cols; j++) {
payoff += adjustedMatrix[i][j] * p2Strategy[j];
}
expectedPayoffs.push(payoff);
}
// Find Player 1's best response
const maxPayoff = Math.max(...expectedPayoffs);
const bestResponses = expectedPayoffs
.map((value, index) => (Math.abs(value - maxPayoff) < 1e-10 ? index : -1))
.filter((index) => index !== -1);
// Create Player 1's mixed strategy
const p1Strategy = Array(rows).fill(0);
for (const index of bestResponses) {
p1Strategy[index] = 1 / bestResponses.length;
}
// Calculate the value of the game
let gameValue = 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
gameValue += payoffMatrix[i][j] * p1Strategy[i] * p2Strategy[j];
}
}
return {
type: 'mixed',
player1Strategy: p1Strategy,
player2Strategy: p2Strategy,
value: gameValue
};
};
/**
* Calculate the expected payoff for a given strategy profile
* @param {Array} player1Strategy - Mixed strategy for player 1
* @param {Array} player2Strategy - Mixed strategy for player 2
* @param {Array} payoffMatrix1 - Payoff matrix for player 1
* @param {Array} [payoffMatrix2=null] - Payoff matrix for player 2 (null for zero-sum)
* @returns {Array} - Expected payoffs [player1Payoff, player2Payoff]
*/
gameTheory.expectedPayoff = function (player1Strategy, player2Strategy, payoffMatrix1, payoffMatrix2 = null) {
const rows = payoffMatrix1.length;
const cols = payoffMatrix1[0].length;
// Validate inputs
if (player1Strategy.length !== rows || player2Strategy.length !== cols) {
throw new Error('Strategy dimensions do not match payoff matrix');
}
if (payoffMatrix2 && (payoffMatrix2.length !== rows || payoffMatrix2[0].length !== cols)) {
throw new Error('Player 2 payoff matrix dimensions do not match player 1 payoff matrix');
}
// Calculate player 1's expected payoff
let player1Payoff = 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
player1Payoff += player1Strategy[i] * player2Strategy[j] * payoffMatrix1[i][j];
}
}
// Calculate player 2's expected payoff
let player2Payoff;
if (payoffMatrix2) {
player2Payoff = 0;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
player2Payoff += player1Strategy[i] * player2Strategy[j] * payoffMatrix2[i][j];
}
}
} else {
// Zero-sum game
player2Payoff = -player1Payoff;
}
return [player1Payoff, player2Payoff];
};
/**
* Find all pure strategy Nash equilibria in a bimatrix game
* @param {Array} payoffMatrix1 - Payoff matrix for player 1
* @param {Array} payoffMatrix2 - Payoff matrix for player 2
* @returns {Array} - Array of Nash equilibrium strategy profiles
*/
gameTheory.findPureNashEquilibria = function (payoffMatrix1, payoffMatrix2) {
const rows = payoffMatrix1.length;
const cols = payoffMatrix1[0].length;
if (payoffMatrix2.length !== rows || payoffMatrix2[0].length !== cols) {
throw new Error('Payoff matrices must have the same dimensions');
}
const equilibria = [];
// For each strategy profile (i,j), check if it's a Nash equilibrium
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
let isNash = true;
// Check if player 1 can deviate
for (let i2 = 0; i2 < rows; i2++) {
if (i2 !== i && payoffMatrix1[i2][j] > payoffMatrix1[i][j]) {
isNash = false;
break;
}
}
if (!isNash) continue;
// Check if player 2 can deviate
for (let j2 = 0; j2 < cols; j2++) {
if (j2 !== j && payoffMatrix2[i][j2] > payoffMatrix2[i][j]) {
isNash = false;
break;
}
}
if (isNash) {
equilibria.push({
profile: [i, j],
payoffs: [payoffMatrix1[i][j], payoffMatrix2[i][j]]
});
}
}
}
return equilibria;
};
/**
* Find approximate mixed strategy Nash equilibrium using iterative algorithm
* @param {Array} payoffMatrix1 - Payoff matrix for player 1
* @param {Array} payoffMatrix2 - Payoff matrix for player 2
* @param {Object} [options={}] - Options for the algorithm
* @returns {Object} - Approximate Nash equilibrium
*/
gameTheory.findMixedNashEquilibrium = function (payoffMatrix1, payoffMatrix2, options = {}) {
const rows = payoffMatrix1.length;
const cols = payoffMatrix1[0].length;
const maxIterations = options.maxIterations || 10000;
const tolerance = options.tolerance || 1e-6;
const learningRate = options.learningRate || 0.01;
// Initialize mixed strategies (uniform distribution)
let p1Strategy = Array(rows).fill(1 / rows);
let p2Strategy = Array(cols).fill(1 / cols);
// Fictitious play algorithm
const p1History = Array(rows).fill(0);
const p2History = Array(cols).fill(0);
for (let iter = 0; iter < maxIterations; iter++) {
// Player 1's best response
const p1Payoffs = Array(rows).fill(0);
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
p1Payoffs[i] += payoffMatrix1[i][j] * p2Strategy[j];
}
}
const p1BestResponse = p1Payoffs.indexOf(Math.max(...p1Payoffs));
p1History[p1BestResponse]++;
// Player 2's best response
const p2Payoffs = Array(cols).fill(0);
for (let j = 0; j < cols; j++) {
for (let i = 0; i < rows; i++) {
p2Payoffs[j] += payoffMatrix2[i][j] * p1Strategy[i];
}
}
const p2BestResponse = p2Payoffs.indexOf(Math.max(...p2Payoffs));
p2History[p2BestResponse]++;
// Update mixed strategies based on history (empirical frequencies)
const p1Total = p1History.reduce((acc, val) => acc + val, 0);
const p2Total = p2History.reduce((acc, val) => acc + val, 0);
const newP1Strategy = p1History.map((val) => val / p1Total);
const newP2Strategy = p2History.map((val) => val / p2Total);
// Check convergence
let p1Change = 0;
let p2Change = 0;
for (let i = 0; i < rows; i++) {
p1Change += Math.abs(newP1Strategy[i] - p1Strategy[i]);
}
for (let j = 0; j < cols; j++) {
p2Change += Math.abs(newP2Strategy[j] - p2Strategy[j]);
}
p1Strategy = newP1Strategy;
p2Strategy = newP2Strategy;
if (p1Change < tolerance && p2Change < tolerance) {
break;
}
}
// Calculate the payoffs
const payoffs = this.expectedPayoff(p1Strategy, p2Strategy, payoffMatrix1, payoffMatrix2);
return {
player1Strategy: p1Strategy,
player2Strategy: p2Strategy,
payoffs
};
};
/**
* Convert a game to its normal form
* @param {Array} strategies1 - Array of strategies for player 1
* @param {Array} strategies2 - Array of strategies for player 2
* @param {Function} utility1 - Utility function for player 1
* @param {Function} utility2 - Utility function for player 2
* @returns {Object} - Normal form representation of the game
*/
gameTheory.toNormalForm = function (strategies1, strategies2, utility1, utility2) {
const rows = strategies1.length;
const cols = strategies2.length;
const payoffMatrix1 = Array(rows).fill().map(() => Array(cols).fill(0));
const payoffMatrix2 = Array(rows).fill().map(() => Array(cols).fill(0));
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
payoffMatrix1[i][j] = utility1(strategies1[i], strategies2[j]);
payoffMatrix2[i][j] = utility2(strategies1[i], strategies2[j]);
}
}
return {
strategies1,
strategies2,
payoffMatrix1,
payoffMatrix2
};
};
/**
* Find the Pareto optimal outcomes in a game
* @param {Array} payoffMatrix1 - Payoff matrix for player 1
* @param {Array} payoffMatrix2 - Payoff matrix for player 2
* @returns {Array} - Array of Pareto optimal outcomes
*/
gameTheory.paretoOptimal = function (payoffMatrix1, payoffMatrix2) {
const rows = payoffMatrix1.length;
const cols = payoffMatrix1[0].length;
if (payoffMatrix2.length !== rows || payoffMatrix2[0].length !== cols) {
throw new Error('Payoff matrices must have the same dimensions');
}
const paretoOptimal = [];
// Check each strategy profile
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const current = [payoffMatrix1[i][j], payoffMatrix2[i][j]];
let isParetoOptimal = true;
// Check if any other outcome Pareto dominates this one
outer: for (let i2 = 0; i2 < rows; i2++) {
for (let j2 = 0; j2 < cols; j2++) {
if (i2 === i && j2 === j) continue;
const other = [payoffMatrix1[i2][j2], payoffMatrix2[i2][j2]];
// Check if other weakly dominates current
if (other[0] >= current[0] && other[1] >= current[1]
&& (other[0] > current[0] || other[1] > current[1])) {
isParetoOptimal = false;
break outer;
}
}
}
if (isParetoOptimal) {
paretoOptimal.push({
profile: [i, j],
payoffs: current
});
}
}
}
return paretoOptimal;
};
/**
* Calculate the social welfare of an outcome
* @param {Array} outcome - Payoffs for each player
* @param {string} [type='utilitarian'] - Type of social welfare ('utilitarian' or 'egalitarian')
* @returns {number} - Social welfare measure
*/
gameTheory.socialWelfare = function (outcome, type = 'utilitarian') {
if (type === 'utilitarian') {
// Sum of utilities
return outcome.reduce((acc, val) => acc + val, 0);
} if (type === 'egalitarian') {
// Minimum utility
return Math.min(...outcome);
}
throw new Error('Unknown social welfare type');
};
/**
* Calculate the price of anarchy
* @param {Array} payoffMatrix1 - Payoff matrix for player 1
* @param {Array} payoffMatrix2 - Payoff matrix for player 2
* @param {string} [welfareType='utilitarian'] - Type of social welfare
* @returns {number} - Price of anarchy
*/
gameTheory.priceOfAnarchy = function (payoffMatrix1, payoffMatrix2, welfareType = 'utilitarian') {
const rows = payoffMatrix1.length;
const cols = payoffMatrix1[0].length;
// Find all Nash equilibria
const equilibria = this.findPureNashEquilibria(payoffMatrix1, payoffMatrix2);
if (equilibria.length === 0) {
return 'No pure Nash equilibria found';
}
// Find the social optimum
let optimalWelfare = -Infinity;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const welfare = this.socialWelfare([payoffMatrix1[i][j], payoffMatrix2[i][j]], welfareType);
optimalWelfare = Math.max(optimalWelfare, welfare);
}
}
// Find the worst Nash equilibrium
let worstEqWelfare = Infinity;
for (const eq of equilibria) {
const welfare = this.socialWelfare(eq.payoffs, welfareType);
worstEqWelfare = Math.min(worstEqWelfare, welfare);
}
// Price of Anarchy = Optimal Welfare / Worst Equilibrium Welfare
return optimalWelfare / worstEqWelfare;
};
/**
* Implement a basic cooperative game solution: the Shapley value
* @param {Function} v - Characteristic function v(coalition)
* @param {Array} players - Array of player identifiers
* @returns {Object} - Shapley value for each player
*/
gameTheory.shapleyValue = function (v, players) {
const n = players.length;
const result = {};
// Initialize Shapley values
for (const player of players) {
result[player] = 0;
}
// Helper function to generate all permutations
function permutations(arr) {
if (arr.length <= 1) return [arr];
const result = [];
for (let i = 0; i < arr.length; i++) {
const current = arr[i];
const remaining = [...arr.slice(0, i), ...arr.slice(i + 1)];
const remainingPerms = permutations(remaining);
for (const perm of remainingPerms) {
result.push([current, ...perm]);
}
}
return result;
}
// Generate all player permutations
const allPermutations = permutations(players);
// For each permutation, calculate marginal contributions
for (const perm of allPermutations) {
const coalition = [];
let prevValue = 0;
for (const player of perm) {
coalition.push(player);
const currentValue = v(coalition);
const marginalContribution = currentValue - prevValue;
result[player] += marginalContribution;
prevValue = currentValue;
}
}
// Average over all permutations
for (const player of players) {
result[player] /= allPermutations.length;
}
return result;
};
/**
* Implement the Banzhaf power index for voting games
* @param {Function} v - Characteristic function v(coalition) (usually 0 or 1)
* @param {Array} players - Array of player identifiers
* @returns {Object} - Banzhaf power index for each player
*/
gameTheory.banzhafIndex = function (v, players) {
const n = players.length;
const result = {};
// Initialize power indices
for (const player of players) {
result[player] = 0;
}
// Generate all subsets of players (the power set)
const powerSet = [[]];
for (const player of players) {
const newSets = [];
for (const set of powerSet) {
newSets.push([...set, player]);
}
powerSet.push(...newSets);
}
// Count critical players in each winning coalition
for (const coalition of powerSet) {
// Skip empty coalition
if (coalition.length === 0) continue;
// Check if coalition is winning
if (v(coalition) === 1) {
// Check each player in the coalition
for (const player of coalition) {
// Remove player from coalition
const coalitionWithoutPlayer = coalition.filter((p) => p !== player);
// If coalition becomes losing, player is critical
if (v(coalitionWithoutPlayer) === 0) {
result[player]++;
}
}
}
}
// Normalize
const total = Object.values(result).reduce((a, b) => a + b, 0);
for (const player of players) {
result[player] /= total;
}
return result;
};
/**
* Implement a basic evolutionary game dynamics simulation
* @param {Array} payoffMatrix - Payoff matrix for all strategies
* @param {Array} initialPopulation - Initial population proportions
* @param {number} iterations - Number of iterations
* @param {number} [mutationRate=0.01] - Rate of random strategy mutation
* @returns {Object} - Time series of population dynamics
*/
gameTheory.replicatorDynamics = function (payoffMatrix, initialPopulation, iterations, mutationRate = 0.01) {
const strategies = initialPopulation.length;
const population = [...initialPopulation];
const timeSeries = [population.map((x) => x)];
for (let t = 0; t < iterations; t++) {
// Calculate fitness for each strategy
const fitness = Array(strategies).fill(0);
for (let i = 0; i < strategies; i++) {
for (let j = 0; j < strategies; j++) {
fitness[i] += payoffMatrix[i][j] * population[j];
}
}
// Calculate average fitness
const avgFitness = fitness.reduce((avg, f, i) => avg + f * population[i], 0);
// Update population proportions
const newPopulation = Array(strategies).fill(0);
for (let i = 0; i < strategies; i++) {
// Replicator equation
newPopulation[i] = population[i] * (fitness[i] / avgFitness);
// Add mutation
if (mutationRate > 0) {
const mutationOut = population[i] * mutationRate;
const mutationIn = (1 - population[i]) * mutationRate / (strategies - 1);
newPopulation[i] = newPopulation[i] * (1 - mutationRate) + mutationIn;
}
}
// Normalize
const sum = newPopulation.reduce((a, b) => a + b, 0);
for (let i = 0; i < strategies; i++) {
population[i] = newPopulation[i] / sum;
}
timeSeries.push(population.map((x) => x));
}
return {
timeSeries,
finalPopulation: population
};
};
module.exports = gameTheory;