import {
PRESETS,
GAME_TYPE_COLORS,
GAME_TYPE_DESCRIPTIONS,
buildMatrix,
classifyFull,
} from "./classifier.mjs";
const INPUT_IDS = [
"r0c0_p1",
"r0c0_p2",
"r0c1_p1",
"r0c1_p2",
"r1c0_p1",
"r1c0_p2",
"r1c1_p1",
"r1c1_p2",
];
const DEFAULT_PRESET = "Prisoner's Dilemma";
function init() {
const presetSelect = document.querySelector("#preset");
const analyzeButton = document.querySelector("#analyze");
const resetButton = document.querySelector("#reset");
for (const name of Object.keys(PRESETS)) {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
presetSelect.append(option);
}
presetSelect.value = DEFAULT_PRESET;
applyPreset(DEFAULT_PRESET);
presetSelect.addEventListener("change", () => {
applyPreset(presetSelect.value);
});
analyzeButton.addEventListener("click", render);
resetButton.addEventListener("click", () => {
presetSelect.value = DEFAULT_PRESET;
applyPreset(DEFAULT_PRESET);
});
for (const inputId of INPUT_IDS) {
document.querySelector(`#${inputId}`).addEventListener("input", render);
}
}
function applyPreset(name) {
const values = PRESETS[name];
if (!values) {
return;
}
INPUT_IDS.forEach((inputId, index) => {
document.querySelector(`#${inputId}`).value = String(values[index]);
});
render();
}
function currentPayoffs() {
return INPUT_IDS.map((inputId) => {
const raw = document.querySelector(`#${inputId}`).value;
const parsed = Number.parseInt(raw, 10);
return Number.isNaN(parsed) ? 0 : parsed;
});
}
function render() {
const payoffs = currentPayoffs();
const matrix = buildMatrix(payoffs);
const result = classifyFull(matrix);
renderMatrix(matrix, result.ne);
renderSummary(matrix, result);
renderClassification(result);
renderProperties(result.props, result.ne);
}
function renderMatrix(matrix, equilibria) {
const neKeys = new Set(equilibria.map(([row, col]) => `${row}-${col}`));
const tbody = document.querySelector("#matrix-body");
tbody.innerHTML = "";
for (let row = 0; row < 2; row += 1) {
const tr = document.createElement("tr");
for (let col = 0; col < 2; col += 1) {
const td = document.createElement("td");
const [p1, p2] = matrix[row][col];
const isNe = neKeys.has(`${row}-${col}`);
td.className = isNe ? "matrix-cell is-ne" : "matrix-cell";
td.innerHTML = `
Row ${row} / Col ${col}
(${p1}, ${p2})
${isNe ? "NASH EQUILIBRIUM" : " "}
`;
tr.append(td);
}
tbody.append(tr);
}
}
function renderSummary(matrix, result) {
const summary = document.querySelector("#summary");
const positions = result.ne.map(([row, col]) => `(${row}, ${col})`).join(", ");
if (result.ne.length === 0) {
if (result.props.mixed_exists) {
summary.innerHTML = `
Pure NE: none
Mixed NE: P1 Row 0 = ${formatNumber(result.props.mixed_p)}, P2 Col 0 = ${formatNumber(result.props.mixed_q)}
Expected payoffs: (${formatNumber(result.props.mixed_payoff_p1)}, ${formatNumber(result.props.mixed_payoff_p2)})
`;
return;
}
summary.innerHTML = `
Pure NE: none
Mixed NE: degenerate
`;
return;
}
if (result.ne.length === 1) {
const [row, col] = result.ne[0];
summary.innerHTML = `
Pure NE: 1
Position: (${row}, ${col})
Payoffs: (${matrix[row][col][0]}, ${matrix[row][col][1]})
`;
return;
}
summary.innerHTML = `
Pure NE: ${result.ne.length}
Positions: ${positions}
Best NE welfare: ${Math.max(...result.props.ne_welfare)}
`;
}
function renderClassification(result) {
const label = result.label;
const badge = document.querySelector("#game-type-badge");
const desc = document.querySelector("#game-type-description");
const color = GAME_TYPE_COLORS[label] || "#7A7570";
badge.textContent = label;
badge.style.color = color;
badge.style.borderColor = color;
desc.textContent = GAME_TYPE_DESCRIPTIONS[label] || "";
}
function renderProperties(props, equilibria) {
const rows = [
propertyRow("P1 dominant strategy", boolBadge(props.p1_has_dominant), "weakly best in every column"),
propertyRow("P2 dominant strategy", boolBadge(props.p2_has_dominant), "weakly best in every row"),
propertyRow("Both dominant", boolBadge(props.both_dominant), ""),
propertyRow("Zero-sum", boolBadge(props.is_zero_sum), "payoff sum constant across cells"),
propertyRow("Symmetric", boolBadge(props.is_symmetric), "payoff swap across diagonal"),
propertyRow("Pure NE count", String(props.ne_count), equilibria.length ? formatPositions(equilibria) : "none"),
propertyRow("Any NE Pareto-dominated", boolBadge(props.has_pareto_dom_ne), ""),
propertyRow("All NE Pareto-efficient", boolBadge(props.all_ne_pareto_eff), ""),
propertyRow("Max social welfare", String(props.max_welfare), "best p1 + p2"),
propertyRow("NE welfare", props.ne_welfare.length ? props.ne_welfare.join(", ") : "-", ""),
propertyRow("Welfare loss", String(props.welfare_loss), "max welfare minus best NE welfare"),
];
if (props.ne_count > 0) {
rows.push(
propertyRow("NE payoffs P1", props.ne_p1_payoffs.join(", "), "per equilibrium"),
propertyRow("NE payoffs P2", props.ne_p2_payoffs.join(", "), "per equilibrium"),
propertyRow("Payoff diff (P1-P2)", props.ne_payoff_diffs.join(", "), "per equilibrium"),
propertyRow("Any NE equal payoffs", boolBadge(props.ne_has_equal_payoffs), ""),
propertyRow("Mean abs diff at NE", props.ne_mean_abs_diff === null ? "-" : formatNumber(props.ne_mean_abs_diff), "")
);
}
if (props.ne_count === 0) {
if (props.mixed_exists) {
rows.push(
propertyRow("Mixed P1 plays Row 0", `p = ${formatNumber(props.mixed_p)}`, ""),
propertyRow("Mixed P2 plays Col 0", `q = ${formatNumber(props.mixed_q)}`, ""),
propertyRow(
"Mixed expected payoffs",
`(${formatNumber(props.mixed_payoff_p1)}, ${formatNumber(props.mixed_payoff_p2)})`,
""
)
);
} else {
rows.push(propertyRow("Mixed strategy", "degenerate", "denominator zero"));
}
}
document.querySelector("#properties-body").innerHTML = rows.join("");
}
function propertyRow(label, value, note) {
return `
| ${label} |
${value} |
${note} |
`;
}
function boolBadge(value) {
return `${value ? "YES" : "NO"}`;
}
function formatPositions(positions) {
return positions.map(([row, col]) => `(${row}, ${col})`).join(", ");
}
function formatNumber(value) {
if (Number.isInteger(value)) {
return String(value);
}
return Number(value).toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
}
init();