AlexDunstan's picture
Deploy plain static Space bundle
1dbf2d8 verified
Raw
History Blame Contribute Delete
12.2 kB
export const PRESETS = {
"Prisoner's Dilemma": [3, 3, 0, 5, 5, 0, 1, 1],
"Stag Hunt": [4, 4, 1, 3, 3, 1, 2, 2],
"Chicken": [3, 3, 1, 4, 4, 1, 0, 0],
"Coordination Game": [2, 2, 0, 0, 0, 0, 2, 2],
"Battle of the Sexes": [3, 2, 0, 0, 0, 0, 2, 3],
"Matching Pennies": [1, -1, -1, 1, -1, 1, 1, -1],
"All Equal (4 NE)": [3, 3, 3, 3, 3, 3, 3, 3],
};
export const GAME_TYPE_COLORS = {
"Prisoner's Dilemma": "#E8610A",
Harmony: "#4CAF82",
Deadlock: "#C0392B",
"Battle of the Sexes": "#D97706",
"Stag Hunt": "#1D8A6B",
Chicken: "#C84B31",
Coordination: "#3A9BD5",
"Zero-Sum": "#9B59B6",
"Dominant (P1 only)": "#F39C12",
"Dominant (P2 only)": "#E67E22",
"No Equilibrium": "#7A7570",
Other: "#4A4540",
};
export const GAME_TYPE_DESCRIPTIONS = {
"Zero-Sum":
"A zero-sum game: one player's gain is exactly the other's loss. The total welfare is constant across all outcomes. Classic examples: chess, poker, matching pennies.",
"Prisoner's Dilemma":
"A social dilemma: both players have a dominant strategy, but the Nash equilibrium leaves both worse off than if they had cooperated. Rational individual behaviour produces a collectively suboptimal result.",
Harmony:
"A harmony game: both players have dominant strategies and the Nash equilibrium is Pareto-efficient. Rational self-interest aligns with the socially optimal outcome.",
Deadlock:
"Both players have dominant strategies leading to an equilibrium, but unlike the Prisoner's Dilemma the cooperative outcome is not better for both. Mutual defection is both rational and efficient.",
"Battle of the Sexes":
"An asymmetric coordination game with two diagonal equilibria. Both players want to coordinate, but each prefers a different equilibrium.",
"Stag Hunt":
"A symmetric coordination game with one high-reward cooperative equilibrium and one safer fallback equilibrium. Trust matters because failing to coordinate can be costly.",
Chicken:
"A symmetric anti-coordination game with off-diagonal equilibria. Each player wants the other side to yield, creating brinkmanship instead of stable mutual cooperation.",
Coordination:
"A coordination-style game: multiple Nash equilibria exist and the main strategic problem is choosing which stable outcome to coordinate on.",
"Dominant (P1 only)":
"Only Player 1 has a dominant strategy. Player 2's best response depends on what Player 1 does, but Player 1 always plays the same way.",
"Dominant (P2 only)":
"Only Player 2 has a dominant strategy. Player 1's best response depends on what Player 2 does, but Player 2 always plays the same way.",
"No Equilibrium":
"No pure-strategy Nash equilibrium exists. Best responses cycle, so the stable object is a mixed-strategy equilibrium.",
Other:
"A game that does not fit neatly into the classic taxonomy. Neither player has a dominant strategy and there is at least one pure-strategy Nash equilibrium.",
};
export function buildMatrix(payoffs) {
return [
[[payoffs[0], payoffs[1]], [payoffs[2], payoffs[3]]],
[[payoffs[4], payoffs[5]], [payoffs[6], payoffs[7]]],
];
}
export function findNashEquilibria(matrix) {
const equilibria = [];
for (let row = 0; row < matrix.length; row += 1) {
for (let col = 0; col < matrix[row].length; col += 1) {
const p1 = matrix[row][col][0];
const p2 = matrix[row][col][1];
let rowBest = true;
for (let otherRow = 0; otherRow < matrix.length; otherRow += 1) {
if (matrix[otherRow][col][0] > p1) {
rowBest = false;
break;
}
}
let colBest = true;
for (let otherCol = 0; otherCol < matrix[row].length; otherCol += 1) {
if (matrix[row][otherCol][1] > p2) {
colBest = false;
break;
}
}
if (rowBest && colBest) {
equilibria.push([row, col]);
}
}
}
return equilibria;
}
function hasDominantStrategy(matrix, player) {
const rows = matrix.length;
const cols = matrix[0].length;
if (player === 0) {
for (let candidateRow = 0; candidateRow < rows; candidateRow += 1) {
let dominant = true;
for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
if (matrix[candidateRow][col][0] < matrix[row][col][0]) {
dominant = false;
break;
}
}
if (!dominant) {
break;
}
}
if (dominant) {
return true;
}
}
return false;
}
for (let candidateCol = 0; candidateCol < cols; candidateCol += 1) {
let dominant = true;
for (let row = 0; row < rows; row += 1) {
for (let col = 0; col < cols; col += 1) {
if (matrix[row][candidateCol][1] < matrix[row][col][1]) {
dominant = false;
break;
}
}
if (!dominant) {
break;
}
}
if (dominant) {
return true;
}
}
return false;
}
function paretoDominated(targetRow, targetCol, matrix) {
const hereP1 = matrix[targetRow][targetCol][0];
const hereP2 = matrix[targetRow][targetCol][1];
for (let row = 0; row < matrix.length; row += 1) {
for (let col = 0; col < matrix[row].length; col += 1) {
if (row === targetRow && col === targetCol) {
continue;
}
const thereP1 = matrix[row][col][0];
const thereP2 = matrix[row][col][1];
if (thereP1 >= hereP1 && thereP2 >= hereP2 && (thereP1 > hereP1 || thereP2 > hereP2)) {
return true;
}
}
}
return false;
}
export function computeMixedStrategy2x2(matrix) {
if (matrix.length !== 2 || matrix[0].length !== 2) {
return {
mixed_exists: false,
mixed_p: null,
mixed_q: null,
mixed_payoff_p1: null,
mixed_payoff_p2: null,
};
}
const a = Number(matrix[0][0][0]);
const e = Number(matrix[0][0][1]);
const b = Number(matrix[0][1][0]);
const f = Number(matrix[0][1][1]);
const c = Number(matrix[1][0][0]);
const g = Number(matrix[1][0][1]);
const d = Number(matrix[1][1][0]);
const h = Number(matrix[1][1][1]);
const denomP = e - g - f + h;
const denomQ = a - b - c + d;
if (denomP === 0 || denomQ === 0) {
return {
mixed_exists: false,
mixed_p: null,
mixed_q: null,
mixed_payoff_p1: null,
mixed_payoff_p2: null,
};
}
const eps = 1e-9;
let p = (h - g) / denomP;
let q = (d - b) / denomQ;
if (!(p >= -eps && p <= 1 + eps && q >= -eps && q <= 1 + eps)) {
return {
mixed_exists: false,
mixed_p: null,
mixed_q: null,
mixed_payoff_p1: null,
mixed_payoff_p2: null,
};
}
p = Math.max(0, Math.min(1, p));
q = Math.max(0, Math.min(1, q));
return {
mixed_exists: true,
mixed_p: round(p, 6),
mixed_q: round(q, 6),
mixed_payoff_p1: round(q * a + (1 - q) * b, 6),
mixed_payoff_p2: round(p * e + (1 - p) * g, 6),
};
}
function nePayoffStats(matrix, nePositions) {
if (nePositions.length === 0) {
return {
ne_p1_payoffs: [],
ne_p2_payoffs: [],
ne_payoff_diffs: [],
ne_has_equal_payoffs: false,
ne_mean_abs_diff: null,
};
}
const p1Payoffs = nePositions.map(([row, col]) => matrix[row][col][0]);
const p2Payoffs = nePositions.map(([row, col]) => matrix[row][col][1]);
const diffs = p1Payoffs.map((value, index) => value - p2Payoffs[index]);
const absMean = diffs.reduce((sum, diff) => sum + Math.abs(diff), 0) / diffs.length;
return {
ne_p1_payoffs: p1Payoffs,
ne_p2_payoffs: p2Payoffs,
ne_payoff_diffs: diffs,
ne_has_equal_payoffs: diffs.some((diff) => diff === 0),
ne_mean_abs_diff: absMean,
};
}
export function classifyProperties(matrix, nePositions) {
const p1Dominant = hasDominantStrategy(matrix, 0);
const p2Dominant = hasDominantStrategy(matrix, 1);
const sums = [];
let maxWelfare = -Infinity;
for (let row = 0; row < matrix.length; row += 1) {
for (let col = 0; col < matrix[row].length; col += 1) {
const welfare = matrix[row][col][0] + matrix[row][col][1];
sums.push(welfare);
if (welfare > maxWelfare) {
maxWelfare = welfare;
}
}
}
const isZeroSum = sums.every((value) => value === sums[0]);
let isSymmetric = matrix.length === matrix[0].length;
if (isSymmetric) {
for (let row = 0; row < matrix.length; row += 1) {
for (let col = 0; col < matrix[row].length; col += 1) {
if (matrix[row][col][0] !== matrix[col][row][1]) {
isSymmetric = false;
break;
}
}
if (!isSymmetric) {
break;
}
}
}
const neWelfare = nePositions.map(([row, col]) => matrix[row][col][0] + matrix[row][col][1]);
const maxNeWelfare = neWelfare.length > 0 ? Math.max(...neWelfare) : 0;
const welfareLoss = maxWelfare - maxNeWelfare;
const paretoFlags = nePositions.map(([row, col]) => paretoDominated(row, col, matrix));
const hasParetoDominatedNe = paretoFlags.some(Boolean);
const mixed = nePositions.length === 0 ? computeMixedStrategy2x2(matrix) : {
mixed_exists: false,
mixed_p: null,
mixed_q: null,
mixed_payoff_p1: null,
mixed_payoff_p2: null,
};
const asym = nePayoffStats(matrix, nePositions);
return {
p1_has_dominant: p1Dominant,
p2_has_dominant: p2Dominant,
both_dominant: p1Dominant && p2Dominant,
is_zero_sum: isZeroSum,
is_symmetric: isSymmetric,
ne_count: nePositions.length,
has_pareto_dom_ne: hasParetoDominatedNe,
all_ne_pareto_eff: !hasParetoDominatedNe,
max_welfare: maxWelfare,
ne_welfare: neWelfare,
welfare_loss: welfareLoss,
mixed_exists: mixed.mixed_exists,
mixed_p: mixed.mixed_p,
mixed_q: mixed.mixed_q,
mixed_payoff_p1: mixed.mixed_payoff_p1,
mixed_payoff_p2: mixed.mixed_payoff_p2,
ne_p1_payoffs: asym.ne_p1_payoffs,
ne_p2_payoffs: asym.ne_p2_payoffs,
ne_payoff_diffs: asym.ne_payoff_diffs,
ne_has_equal_payoffs: asym.ne_has_equal_payoffs,
ne_mean_abs_diff: asym.ne_mean_abs_diff,
};
}
function isDiagonalPair(nePositions) {
return nePositions.length === 2
&& nePositions.some(([row, col]) => row === 0 && col === 0)
&& nePositions.some(([row, col]) => row === 1 && col === 1);
}
function isOffDiagonalPair(nePositions) {
return nePositions.length === 2
&& nePositions.some(([row, col]) => row === 0 && col === 1)
&& nePositions.some(([row, col]) => row === 1 && col === 0);
}
export function classifyGameType(props, nePositions, matrix) {
if (props.is_zero_sum) {
return "Zero-Sum";
}
if (props.both_dominant) {
if (props.has_pareto_dom_ne && props.welfare_loss > 0) {
return "Prisoner's Dilemma";
}
if (props.welfare_loss === 0) {
return "Harmony";
}
return "Deadlock";
}
if (matrix && isDiagonalPair(nePositions)) {
const tl = matrix[0][0];
const br = matrix[1][1];
const p1PrefersTl = tl[0] > br[0];
const p1PrefersBr = br[0] > tl[0];
const p2PrefersTl = tl[1] > br[1];
const p2PrefersBr = br[1] > tl[1];
if ((p1PrefersTl && p2PrefersBr) || (p1PrefersBr && p2PrefersTl)) {
return "Battle of the Sexes";
}
if (props.is_symmetric) {
const tlWelfare = tl[0] + tl[1];
const brWelfare = br[0] + br[1];
if (tlWelfare !== brWelfare) {
return "Stag Hunt";
}
}
return "Coordination";
}
if (matrix && isOffDiagonalPair(nePositions) && props.is_symmetric) {
return "Chicken";
}
if (nePositions.length >= 2 && props.is_symmetric) {
return "Coordination";
}
if (props.p1_has_dominant && !props.p2_has_dominant) {
return "Dominant (P1 only)";
}
if (props.p2_has_dominant && !props.p1_has_dominant) {
return "Dominant (P2 only)";
}
if (props.ne_count === 0) {
return "No Equilibrium";
}
return "Other";
}
export function classifyFull(matrix) {
const ne = findNashEquilibria(matrix);
const props = classifyProperties(matrix, ne);
const label = classifyGameType(props, ne, matrix);
return { ne, props, label };
}
function round(value, digits) {
const factor = 10 ** digits;
return Math.round(value * factor) / factor;
}