Deploy plain static Space bundle
Browse filesReplace broken Gradio Lite build with plain static JS bundle and remove stale public files.
- README.md +1 -1
- app.mjs +226 -0
- classifier.mjs +399 -0
- index.html +126 -1469
- style.css +0 -28
- styles.css +340 -0
README.md
CHANGED
|
@@ -12,7 +12,7 @@ pinned: false
|
|
| 12 |
|
| 13 |
This Space is a compact public-facing demo for exploring 2x2 payoff matrices.
|
| 14 |
|
| 15 |
-
It is packaged as a static Hugging Face Space
|
| 16 |
|
| 17 |
It does three things:
|
| 18 |
|
|
|
|
| 12 |
|
| 13 |
This Space is a compact public-facing demo for exploring 2x2 payoff matrices.
|
| 14 |
|
| 15 |
+
It is packaged as a plain static Hugging Face Space, so the app runs client-side in the browser with no Python runtime and no Gradio dependency.
|
| 16 |
|
| 17 |
It does three things:
|
| 18 |
|
app.mjs
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import {
|
| 2 |
+
PRESETS,
|
| 3 |
+
GAME_TYPE_COLORS,
|
| 4 |
+
GAME_TYPE_DESCRIPTIONS,
|
| 5 |
+
buildMatrix,
|
| 6 |
+
classifyFull,
|
| 7 |
+
} from "./classifier.mjs";
|
| 8 |
+
|
| 9 |
+
const INPUT_IDS = [
|
| 10 |
+
"r0c0_p1",
|
| 11 |
+
"r0c0_p2",
|
| 12 |
+
"r0c1_p1",
|
| 13 |
+
"r0c1_p2",
|
| 14 |
+
"r1c0_p1",
|
| 15 |
+
"r1c0_p2",
|
| 16 |
+
"r1c1_p1",
|
| 17 |
+
"r1c1_p2",
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
const DEFAULT_PRESET = "Prisoner's Dilemma";
|
| 21 |
+
|
| 22 |
+
function init() {
|
| 23 |
+
const presetSelect = document.querySelector("#preset");
|
| 24 |
+
const analyzeButton = document.querySelector("#analyze");
|
| 25 |
+
const resetButton = document.querySelector("#reset");
|
| 26 |
+
|
| 27 |
+
for (const name of Object.keys(PRESETS)) {
|
| 28 |
+
const option = document.createElement("option");
|
| 29 |
+
option.value = name;
|
| 30 |
+
option.textContent = name;
|
| 31 |
+
presetSelect.append(option);
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
presetSelect.value = DEFAULT_PRESET;
|
| 35 |
+
applyPreset(DEFAULT_PRESET);
|
| 36 |
+
|
| 37 |
+
presetSelect.addEventListener("change", () => {
|
| 38 |
+
applyPreset(presetSelect.value);
|
| 39 |
+
});
|
| 40 |
+
|
| 41 |
+
analyzeButton.addEventListener("click", render);
|
| 42 |
+
resetButton.addEventListener("click", () => {
|
| 43 |
+
presetSelect.value = DEFAULT_PRESET;
|
| 44 |
+
applyPreset(DEFAULT_PRESET);
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
for (const inputId of INPUT_IDS) {
|
| 48 |
+
document.querySelector(`#${inputId}`).addEventListener("input", render);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function applyPreset(name) {
|
| 53 |
+
const values = PRESETS[name];
|
| 54 |
+
if (!values) {
|
| 55 |
+
return;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
INPUT_IDS.forEach((inputId, index) => {
|
| 59 |
+
document.querySelector(`#${inputId}`).value = String(values[index]);
|
| 60 |
+
});
|
| 61 |
+
|
| 62 |
+
render();
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
function currentPayoffs() {
|
| 66 |
+
return INPUT_IDS.map((inputId) => {
|
| 67 |
+
const raw = document.querySelector(`#${inputId}`).value;
|
| 68 |
+
const parsed = Number.parseInt(raw, 10);
|
| 69 |
+
return Number.isNaN(parsed) ? 0 : parsed;
|
| 70 |
+
});
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function render() {
|
| 74 |
+
const payoffs = currentPayoffs();
|
| 75 |
+
const matrix = buildMatrix(payoffs);
|
| 76 |
+
const result = classifyFull(matrix);
|
| 77 |
+
|
| 78 |
+
renderMatrix(matrix, result.ne);
|
| 79 |
+
renderSummary(matrix, result);
|
| 80 |
+
renderClassification(result);
|
| 81 |
+
renderProperties(result.props, result.ne);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
function renderMatrix(matrix, equilibria) {
|
| 85 |
+
const neKeys = new Set(equilibria.map(([row, col]) => `${row}-${col}`));
|
| 86 |
+
const tbody = document.querySelector("#matrix-body");
|
| 87 |
+
tbody.innerHTML = "";
|
| 88 |
+
|
| 89 |
+
for (let row = 0; row < 2; row += 1) {
|
| 90 |
+
const tr = document.createElement("tr");
|
| 91 |
+
for (let col = 0; col < 2; col += 1) {
|
| 92 |
+
const td = document.createElement("td");
|
| 93 |
+
const [p1, p2] = matrix[row][col];
|
| 94 |
+
const isNe = neKeys.has(`${row}-${col}`);
|
| 95 |
+
td.className = isNe ? "matrix-cell is-ne" : "matrix-cell";
|
| 96 |
+
td.innerHTML = `
|
| 97 |
+
<div class="cell-coord">Row ${row} / Col ${col}</div>
|
| 98 |
+
<div class="cell-payoff">(${p1}, ${p2})</div>
|
| 99 |
+
<div class="cell-tag">${isNe ? "NASH EQUILIBRIUM" : " "}</div>
|
| 100 |
+
`;
|
| 101 |
+
tr.append(td);
|
| 102 |
+
}
|
| 103 |
+
tbody.append(tr);
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
function renderSummary(matrix, result) {
|
| 108 |
+
const summary = document.querySelector("#summary");
|
| 109 |
+
const positions = result.ne.map(([row, col]) => `(${row}, ${col})`).join(", ");
|
| 110 |
+
|
| 111 |
+
if (result.ne.length === 0) {
|
| 112 |
+
if (result.props.mixed_exists) {
|
| 113 |
+
summary.innerHTML = `
|
| 114 |
+
<div class="summary-line">Pure NE: none</div>
|
| 115 |
+
<div class="summary-line">Mixed NE: P1 Row 0 = <strong>${formatNumber(result.props.mixed_p)}</strong>, P2 Col 0 = <strong>${formatNumber(result.props.mixed_q)}</strong></div>
|
| 116 |
+
<div class="summary-line">Expected payoffs: <strong>(${formatNumber(result.props.mixed_payoff_p1)}, ${formatNumber(result.props.mixed_payoff_p2)})</strong></div>
|
| 117 |
+
`;
|
| 118 |
+
return;
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
summary.innerHTML = `
|
| 122 |
+
<div class="summary-line">Pure NE: none</div>
|
| 123 |
+
<div class="summary-line">Mixed NE: degenerate</div>
|
| 124 |
+
`;
|
| 125 |
+
return;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
if (result.ne.length === 1) {
|
| 129 |
+
const [row, col] = result.ne[0];
|
| 130 |
+
summary.innerHTML = `
|
| 131 |
+
<div class="summary-line">Pure NE: <strong>1</strong></div>
|
| 132 |
+
<div class="summary-line">Position: <strong>(${row}, ${col})</strong></div>
|
| 133 |
+
<div class="summary-line">Payoffs: <strong>(${matrix[row][col][0]}, ${matrix[row][col][1]})</strong></div>
|
| 134 |
+
`;
|
| 135 |
+
return;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
summary.innerHTML = `
|
| 139 |
+
<div class="summary-line">Pure NE: <strong>${result.ne.length}</strong></div>
|
| 140 |
+
<div class="summary-line">Positions: <strong>${positions}</strong></div>
|
| 141 |
+
<div class="summary-line">Best NE welfare: <strong>${Math.max(...result.props.ne_welfare)}</strong></div>
|
| 142 |
+
`;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
function renderClassification(result) {
|
| 146 |
+
const label = result.label;
|
| 147 |
+
const badge = document.querySelector("#game-type-badge");
|
| 148 |
+
const desc = document.querySelector("#game-type-description");
|
| 149 |
+
const color = GAME_TYPE_COLORS[label] || "#7A7570";
|
| 150 |
+
|
| 151 |
+
badge.textContent = label;
|
| 152 |
+
badge.style.color = color;
|
| 153 |
+
badge.style.borderColor = color;
|
| 154 |
+
desc.textContent = GAME_TYPE_DESCRIPTIONS[label] || "";
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function renderProperties(props, equilibria) {
|
| 158 |
+
const rows = [
|
| 159 |
+
propertyRow("P1 dominant strategy", boolBadge(props.p1_has_dominant), "weakly best in every column"),
|
| 160 |
+
propertyRow("P2 dominant strategy", boolBadge(props.p2_has_dominant), "weakly best in every row"),
|
| 161 |
+
propertyRow("Both dominant", boolBadge(props.both_dominant), ""),
|
| 162 |
+
propertyRow("Zero-sum", boolBadge(props.is_zero_sum), "payoff sum constant across cells"),
|
| 163 |
+
propertyRow("Symmetric", boolBadge(props.is_symmetric), "payoff swap across diagonal"),
|
| 164 |
+
propertyRow("Pure NE count", String(props.ne_count), equilibria.length ? formatPositions(equilibria) : "none"),
|
| 165 |
+
propertyRow("Any NE Pareto-dominated", boolBadge(props.has_pareto_dom_ne), ""),
|
| 166 |
+
propertyRow("All NE Pareto-efficient", boolBadge(props.all_ne_pareto_eff), ""),
|
| 167 |
+
propertyRow("Max social welfare", String(props.max_welfare), "best p1 + p2"),
|
| 168 |
+
propertyRow("NE welfare", props.ne_welfare.length ? props.ne_welfare.join(", ") : "-", ""),
|
| 169 |
+
propertyRow("Welfare loss", String(props.welfare_loss), "max welfare minus best NE welfare"),
|
| 170 |
+
];
|
| 171 |
+
|
| 172 |
+
if (props.ne_count > 0) {
|
| 173 |
+
rows.push(
|
| 174 |
+
propertyRow("NE payoffs P1", props.ne_p1_payoffs.join(", "), "per equilibrium"),
|
| 175 |
+
propertyRow("NE payoffs P2", props.ne_p2_payoffs.join(", "), "per equilibrium"),
|
| 176 |
+
propertyRow("Payoff diff (P1-P2)", props.ne_payoff_diffs.join(", "), "per equilibrium"),
|
| 177 |
+
propertyRow("Any NE equal payoffs", boolBadge(props.ne_has_equal_payoffs), ""),
|
| 178 |
+
propertyRow("Mean abs diff at NE", props.ne_mean_abs_diff === null ? "-" : formatNumber(props.ne_mean_abs_diff), "")
|
| 179 |
+
);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
if (props.ne_count === 0) {
|
| 183 |
+
if (props.mixed_exists) {
|
| 184 |
+
rows.push(
|
| 185 |
+
propertyRow("Mixed P1 plays Row 0", `p = ${formatNumber(props.mixed_p)}`, ""),
|
| 186 |
+
propertyRow("Mixed P2 plays Col 0", `q = ${formatNumber(props.mixed_q)}`, ""),
|
| 187 |
+
propertyRow(
|
| 188 |
+
"Mixed expected payoffs",
|
| 189 |
+
`(${formatNumber(props.mixed_payoff_p1)}, ${formatNumber(props.mixed_payoff_p2)})`,
|
| 190 |
+
""
|
| 191 |
+
)
|
| 192 |
+
);
|
| 193 |
+
} else {
|
| 194 |
+
rows.push(propertyRow("Mixed strategy", "degenerate", "denominator zero"));
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
document.querySelector("#properties-body").innerHTML = rows.join("");
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
function propertyRow(label, value, note) {
|
| 202 |
+
return `
|
| 203 |
+
<tr>
|
| 204 |
+
<td>${label}</td>
|
| 205 |
+
<td>${value}</td>
|
| 206 |
+
<td>${note}</td>
|
| 207 |
+
</tr>
|
| 208 |
+
`;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function boolBadge(value) {
|
| 212 |
+
return `<span class="${value ? "bool-yes" : "bool-no"}">${value ? "YES" : "NO"}</span>`;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
function formatPositions(positions) {
|
| 216 |
+
return positions.map(([row, col]) => `(${row}, ${col})`).join(", ");
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
function formatNumber(value) {
|
| 220 |
+
if (Number.isInteger(value)) {
|
| 221 |
+
return String(value);
|
| 222 |
+
}
|
| 223 |
+
return Number(value).toFixed(4).replace(/0+$/, "").replace(/\.$/, "");
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
init();
|
classifier.mjs
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const PRESETS = {
|
| 2 |
+
"Prisoner's Dilemma": [3, 3, 0, 5, 5, 0, 1, 1],
|
| 3 |
+
"Stag Hunt": [4, 4, 1, 3, 3, 1, 2, 2],
|
| 4 |
+
"Chicken": [3, 3, 1, 4, 4, 1, 0, 0],
|
| 5 |
+
"Coordination Game": [2, 2, 0, 0, 0, 0, 2, 2],
|
| 6 |
+
"Battle of the Sexes": [3, 2, 0, 0, 0, 0, 2, 3],
|
| 7 |
+
"Matching Pennies": [1, -1, -1, 1, -1, 1, 1, -1],
|
| 8 |
+
"All Equal (4 NE)": [3, 3, 3, 3, 3, 3, 3, 3],
|
| 9 |
+
};
|
| 10 |
+
|
| 11 |
+
export const GAME_TYPE_COLORS = {
|
| 12 |
+
"Prisoner's Dilemma": "#E8610A",
|
| 13 |
+
Harmony: "#4CAF82",
|
| 14 |
+
Deadlock: "#C0392B",
|
| 15 |
+
"Battle of the Sexes": "#D97706",
|
| 16 |
+
"Stag Hunt": "#1D8A6B",
|
| 17 |
+
Chicken: "#C84B31",
|
| 18 |
+
Coordination: "#3A9BD5",
|
| 19 |
+
"Zero-Sum": "#9B59B6",
|
| 20 |
+
"Dominant (P1 only)": "#F39C12",
|
| 21 |
+
"Dominant (P2 only)": "#E67E22",
|
| 22 |
+
"No Equilibrium": "#7A7570",
|
| 23 |
+
Other: "#4A4540",
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
export const GAME_TYPE_DESCRIPTIONS = {
|
| 27 |
+
"Zero-Sum":
|
| 28 |
+
"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.",
|
| 29 |
+
"Prisoner's Dilemma":
|
| 30 |
+
"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.",
|
| 31 |
+
Harmony:
|
| 32 |
+
"A harmony game: both players have dominant strategies and the Nash equilibrium is Pareto-efficient. Rational self-interest aligns with the socially optimal outcome.",
|
| 33 |
+
Deadlock:
|
| 34 |
+
"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.",
|
| 35 |
+
"Battle of the Sexes":
|
| 36 |
+
"An asymmetric coordination game with two diagonal equilibria. Both players want to coordinate, but each prefers a different equilibrium.",
|
| 37 |
+
"Stag Hunt":
|
| 38 |
+
"A symmetric coordination game with one high-reward cooperative equilibrium and one safer fallback equilibrium. Trust matters because failing to coordinate can be costly.",
|
| 39 |
+
Chicken:
|
| 40 |
+
"A symmetric anti-coordination game with off-diagonal equilibria. Each player wants the other side to yield, creating brinkmanship instead of stable mutual cooperation.",
|
| 41 |
+
Coordination:
|
| 42 |
+
"A coordination-style game: multiple Nash equilibria exist and the main strategic problem is choosing which stable outcome to coordinate on.",
|
| 43 |
+
"Dominant (P1 only)":
|
| 44 |
+
"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.",
|
| 45 |
+
"Dominant (P2 only)":
|
| 46 |
+
"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.",
|
| 47 |
+
"No Equilibrium":
|
| 48 |
+
"No pure-strategy Nash equilibrium exists. Best responses cycle, so the stable object is a mixed-strategy equilibrium.",
|
| 49 |
+
Other:
|
| 50 |
+
"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.",
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
export function buildMatrix(payoffs) {
|
| 54 |
+
return [
|
| 55 |
+
[[payoffs[0], payoffs[1]], [payoffs[2], payoffs[3]]],
|
| 56 |
+
[[payoffs[4], payoffs[5]], [payoffs[6], payoffs[7]]],
|
| 57 |
+
];
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
export function findNashEquilibria(matrix) {
|
| 61 |
+
const equilibria = [];
|
| 62 |
+
for (let row = 0; row < matrix.length; row += 1) {
|
| 63 |
+
for (let col = 0; col < matrix[row].length; col += 1) {
|
| 64 |
+
const p1 = matrix[row][col][0];
|
| 65 |
+
const p2 = matrix[row][col][1];
|
| 66 |
+
|
| 67 |
+
let rowBest = true;
|
| 68 |
+
for (let otherRow = 0; otherRow < matrix.length; otherRow += 1) {
|
| 69 |
+
if (matrix[otherRow][col][0] > p1) {
|
| 70 |
+
rowBest = false;
|
| 71 |
+
break;
|
| 72 |
+
}
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
let colBest = true;
|
| 76 |
+
for (let otherCol = 0; otherCol < matrix[row].length; otherCol += 1) {
|
| 77 |
+
if (matrix[row][otherCol][1] > p2) {
|
| 78 |
+
colBest = false;
|
| 79 |
+
break;
|
| 80 |
+
}
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
if (rowBest && colBest) {
|
| 84 |
+
equilibria.push([row, col]);
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
return equilibria;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
function hasDominantStrategy(matrix, player) {
|
| 92 |
+
const rows = matrix.length;
|
| 93 |
+
const cols = matrix[0].length;
|
| 94 |
+
|
| 95 |
+
if (player === 0) {
|
| 96 |
+
for (let candidateRow = 0; candidateRow < rows; candidateRow += 1) {
|
| 97 |
+
let dominant = true;
|
| 98 |
+
for (let row = 0; row < rows; row += 1) {
|
| 99 |
+
for (let col = 0; col < cols; col += 1) {
|
| 100 |
+
if (matrix[candidateRow][col][0] < matrix[row][col][0]) {
|
| 101 |
+
dominant = false;
|
| 102 |
+
break;
|
| 103 |
+
}
|
| 104 |
+
}
|
| 105 |
+
if (!dominant) {
|
| 106 |
+
break;
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
if (dominant) {
|
| 110 |
+
return true;
|
| 111 |
+
}
|
| 112 |
+
}
|
| 113 |
+
return false;
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
for (let candidateCol = 0; candidateCol < cols; candidateCol += 1) {
|
| 117 |
+
let dominant = true;
|
| 118 |
+
for (let row = 0; row < rows; row += 1) {
|
| 119 |
+
for (let col = 0; col < cols; col += 1) {
|
| 120 |
+
if (matrix[row][candidateCol][1] < matrix[row][col][1]) {
|
| 121 |
+
dominant = false;
|
| 122 |
+
break;
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
if (!dominant) {
|
| 126 |
+
break;
|
| 127 |
+
}
|
| 128 |
+
}
|
| 129 |
+
if (dominant) {
|
| 130 |
+
return true;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
return false;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
function paretoDominated(targetRow, targetCol, matrix) {
|
| 138 |
+
const hereP1 = matrix[targetRow][targetCol][0];
|
| 139 |
+
const hereP2 = matrix[targetRow][targetCol][1];
|
| 140 |
+
|
| 141 |
+
for (let row = 0; row < matrix.length; row += 1) {
|
| 142 |
+
for (let col = 0; col < matrix[row].length; col += 1) {
|
| 143 |
+
if (row === targetRow && col === targetCol) {
|
| 144 |
+
continue;
|
| 145 |
+
}
|
| 146 |
+
const thereP1 = matrix[row][col][0];
|
| 147 |
+
const thereP2 = matrix[row][col][1];
|
| 148 |
+
if (thereP1 >= hereP1 && thereP2 >= hereP2 && (thereP1 > hereP1 || thereP2 > hereP2)) {
|
| 149 |
+
return true;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
return false;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
export function computeMixedStrategy2x2(matrix) {
|
| 158 |
+
if (matrix.length !== 2 || matrix[0].length !== 2) {
|
| 159 |
+
return {
|
| 160 |
+
mixed_exists: false,
|
| 161 |
+
mixed_p: null,
|
| 162 |
+
mixed_q: null,
|
| 163 |
+
mixed_payoff_p1: null,
|
| 164 |
+
mixed_payoff_p2: null,
|
| 165 |
+
};
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
const a = Number(matrix[0][0][0]);
|
| 169 |
+
const e = Number(matrix[0][0][1]);
|
| 170 |
+
const b = Number(matrix[0][1][0]);
|
| 171 |
+
const f = Number(matrix[0][1][1]);
|
| 172 |
+
const c = Number(matrix[1][0][0]);
|
| 173 |
+
const g = Number(matrix[1][0][1]);
|
| 174 |
+
const d = Number(matrix[1][1][0]);
|
| 175 |
+
const h = Number(matrix[1][1][1]);
|
| 176 |
+
|
| 177 |
+
const denomP = e - g - f + h;
|
| 178 |
+
const denomQ = a - b - c + d;
|
| 179 |
+
|
| 180 |
+
if (denomP === 0 || denomQ === 0) {
|
| 181 |
+
return {
|
| 182 |
+
mixed_exists: false,
|
| 183 |
+
mixed_p: null,
|
| 184 |
+
mixed_q: null,
|
| 185 |
+
mixed_payoff_p1: null,
|
| 186 |
+
mixed_payoff_p2: null,
|
| 187 |
+
};
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
const eps = 1e-9;
|
| 191 |
+
let p = (h - g) / denomP;
|
| 192 |
+
let q = (d - b) / denomQ;
|
| 193 |
+
|
| 194 |
+
if (!(p >= -eps && p <= 1 + eps && q >= -eps && q <= 1 + eps)) {
|
| 195 |
+
return {
|
| 196 |
+
mixed_exists: false,
|
| 197 |
+
mixed_p: null,
|
| 198 |
+
mixed_q: null,
|
| 199 |
+
mixed_payoff_p1: null,
|
| 200 |
+
mixed_payoff_p2: null,
|
| 201 |
+
};
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
p = Math.max(0, Math.min(1, p));
|
| 205 |
+
q = Math.max(0, Math.min(1, q));
|
| 206 |
+
|
| 207 |
+
return {
|
| 208 |
+
mixed_exists: true,
|
| 209 |
+
mixed_p: round(p, 6),
|
| 210 |
+
mixed_q: round(q, 6),
|
| 211 |
+
mixed_payoff_p1: round(q * a + (1 - q) * b, 6),
|
| 212 |
+
mixed_payoff_p2: round(p * e + (1 - p) * g, 6),
|
| 213 |
+
};
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
function nePayoffStats(matrix, nePositions) {
|
| 217 |
+
if (nePositions.length === 0) {
|
| 218 |
+
return {
|
| 219 |
+
ne_p1_payoffs: [],
|
| 220 |
+
ne_p2_payoffs: [],
|
| 221 |
+
ne_payoff_diffs: [],
|
| 222 |
+
ne_has_equal_payoffs: false,
|
| 223 |
+
ne_mean_abs_diff: null,
|
| 224 |
+
};
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
const p1Payoffs = nePositions.map(([row, col]) => matrix[row][col][0]);
|
| 228 |
+
const p2Payoffs = nePositions.map(([row, col]) => matrix[row][col][1]);
|
| 229 |
+
const diffs = p1Payoffs.map((value, index) => value - p2Payoffs[index]);
|
| 230 |
+
const absMean = diffs.reduce((sum, diff) => sum + Math.abs(diff), 0) / diffs.length;
|
| 231 |
+
|
| 232 |
+
return {
|
| 233 |
+
ne_p1_payoffs: p1Payoffs,
|
| 234 |
+
ne_p2_payoffs: p2Payoffs,
|
| 235 |
+
ne_payoff_diffs: diffs,
|
| 236 |
+
ne_has_equal_payoffs: diffs.some((diff) => diff === 0),
|
| 237 |
+
ne_mean_abs_diff: absMean,
|
| 238 |
+
};
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
export function classifyProperties(matrix, nePositions) {
|
| 242 |
+
const p1Dominant = hasDominantStrategy(matrix, 0);
|
| 243 |
+
const p2Dominant = hasDominantStrategy(matrix, 1);
|
| 244 |
+
|
| 245 |
+
const sums = [];
|
| 246 |
+
let maxWelfare = -Infinity;
|
| 247 |
+
for (let row = 0; row < matrix.length; row += 1) {
|
| 248 |
+
for (let col = 0; col < matrix[row].length; col += 1) {
|
| 249 |
+
const welfare = matrix[row][col][0] + matrix[row][col][1];
|
| 250 |
+
sums.push(welfare);
|
| 251 |
+
if (welfare > maxWelfare) {
|
| 252 |
+
maxWelfare = welfare;
|
| 253 |
+
}
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
const isZeroSum = sums.every((value) => value === sums[0]);
|
| 257 |
+
|
| 258 |
+
let isSymmetric = matrix.length === matrix[0].length;
|
| 259 |
+
if (isSymmetric) {
|
| 260 |
+
for (let row = 0; row < matrix.length; row += 1) {
|
| 261 |
+
for (let col = 0; col < matrix[row].length; col += 1) {
|
| 262 |
+
if (matrix[row][col][0] !== matrix[col][row][1]) {
|
| 263 |
+
isSymmetric = false;
|
| 264 |
+
break;
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
if (!isSymmetric) {
|
| 268 |
+
break;
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
const neWelfare = nePositions.map(([row, col]) => matrix[row][col][0] + matrix[row][col][1]);
|
| 274 |
+
const maxNeWelfare = neWelfare.length > 0 ? Math.max(...neWelfare) : 0;
|
| 275 |
+
const welfareLoss = maxWelfare - maxNeWelfare;
|
| 276 |
+
|
| 277 |
+
const paretoFlags = nePositions.map(([row, col]) => paretoDominated(row, col, matrix));
|
| 278 |
+
const hasParetoDominatedNe = paretoFlags.some(Boolean);
|
| 279 |
+
|
| 280 |
+
const mixed = nePositions.length === 0 ? computeMixedStrategy2x2(matrix) : {
|
| 281 |
+
mixed_exists: false,
|
| 282 |
+
mixed_p: null,
|
| 283 |
+
mixed_q: null,
|
| 284 |
+
mixed_payoff_p1: null,
|
| 285 |
+
mixed_payoff_p2: null,
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
const asym = nePayoffStats(matrix, nePositions);
|
| 289 |
+
|
| 290 |
+
return {
|
| 291 |
+
p1_has_dominant: p1Dominant,
|
| 292 |
+
p2_has_dominant: p2Dominant,
|
| 293 |
+
both_dominant: p1Dominant && p2Dominant,
|
| 294 |
+
is_zero_sum: isZeroSum,
|
| 295 |
+
is_symmetric: isSymmetric,
|
| 296 |
+
ne_count: nePositions.length,
|
| 297 |
+
has_pareto_dom_ne: hasParetoDominatedNe,
|
| 298 |
+
all_ne_pareto_eff: !hasParetoDominatedNe,
|
| 299 |
+
max_welfare: maxWelfare,
|
| 300 |
+
ne_welfare: neWelfare,
|
| 301 |
+
welfare_loss: welfareLoss,
|
| 302 |
+
mixed_exists: mixed.mixed_exists,
|
| 303 |
+
mixed_p: mixed.mixed_p,
|
| 304 |
+
mixed_q: mixed.mixed_q,
|
| 305 |
+
mixed_payoff_p1: mixed.mixed_payoff_p1,
|
| 306 |
+
mixed_payoff_p2: mixed.mixed_payoff_p2,
|
| 307 |
+
ne_p1_payoffs: asym.ne_p1_payoffs,
|
| 308 |
+
ne_p2_payoffs: asym.ne_p2_payoffs,
|
| 309 |
+
ne_payoff_diffs: asym.ne_payoff_diffs,
|
| 310 |
+
ne_has_equal_payoffs: asym.ne_has_equal_payoffs,
|
| 311 |
+
ne_mean_abs_diff: asym.ne_mean_abs_diff,
|
| 312 |
+
};
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
function isDiagonalPair(nePositions) {
|
| 316 |
+
return nePositions.length === 2
|
| 317 |
+
&& nePositions.some(([row, col]) => row === 0 && col === 0)
|
| 318 |
+
&& nePositions.some(([row, col]) => row === 1 && col === 1);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
function isOffDiagonalPair(nePositions) {
|
| 322 |
+
return nePositions.length === 2
|
| 323 |
+
&& nePositions.some(([row, col]) => row === 0 && col === 1)
|
| 324 |
+
&& nePositions.some(([row, col]) => row === 1 && col === 0);
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
export function classifyGameType(props, nePositions, matrix) {
|
| 328 |
+
if (props.is_zero_sum) {
|
| 329 |
+
return "Zero-Sum";
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
if (props.both_dominant) {
|
| 333 |
+
if (props.has_pareto_dom_ne && props.welfare_loss > 0) {
|
| 334 |
+
return "Prisoner's Dilemma";
|
| 335 |
+
}
|
| 336 |
+
if (props.welfare_loss === 0) {
|
| 337 |
+
return "Harmony";
|
| 338 |
+
}
|
| 339 |
+
return "Deadlock";
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
if (matrix && isDiagonalPair(nePositions)) {
|
| 343 |
+
const tl = matrix[0][0];
|
| 344 |
+
const br = matrix[1][1];
|
| 345 |
+
|
| 346 |
+
const p1PrefersTl = tl[0] > br[0];
|
| 347 |
+
const p1PrefersBr = br[0] > tl[0];
|
| 348 |
+
const p2PrefersTl = tl[1] > br[1];
|
| 349 |
+
const p2PrefersBr = br[1] > tl[1];
|
| 350 |
+
|
| 351 |
+
if ((p1PrefersTl && p2PrefersBr) || (p1PrefersBr && p2PrefersTl)) {
|
| 352 |
+
return "Battle of the Sexes";
|
| 353 |
+
}
|
| 354 |
+
|
| 355 |
+
if (props.is_symmetric) {
|
| 356 |
+
const tlWelfare = tl[0] + tl[1];
|
| 357 |
+
const brWelfare = br[0] + br[1];
|
| 358 |
+
if (tlWelfare !== brWelfare) {
|
| 359 |
+
return "Stag Hunt";
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
return "Coordination";
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
if (matrix && isOffDiagonalPair(nePositions) && props.is_symmetric) {
|
| 367 |
+
return "Chicken";
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
if (nePositions.length >= 2 && props.is_symmetric) {
|
| 371 |
+
return "Coordination";
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
if (props.p1_has_dominant && !props.p2_has_dominant) {
|
| 375 |
+
return "Dominant (P1 only)";
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
if (props.p2_has_dominant && !props.p1_has_dominant) {
|
| 379 |
+
return "Dominant (P2 only)";
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
if (props.ne_count === 0) {
|
| 383 |
+
return "No Equilibrium";
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
return "Other";
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
export function classifyFull(matrix) {
|
| 390 |
+
const ne = findNashEquilibria(matrix);
|
| 391 |
+
const props = classifyProperties(matrix, ne);
|
| 392 |
+
const label = classifyGameType(props, ne, matrix);
|
| 393 |
+
return { ne, props, label };
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
function round(value, digits) {
|
| 397 |
+
const factor = 10 ** digits;
|
| 398 |
+
return Math.round(value * factor) / factor;
|
| 399 |
+
}
|
index.html
CHANGED
|
@@ -4,1476 +4,133 @@
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Game Theory Matrix Classifier</title>
|
| 7 |
-
<meta
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
body {
|
| 16 |
-
background:
|
| 17 |
-
radial-gradient(circle at top right, rgba(232, 97, 10, 0.12), transparent 28rem),
|
| 18 |
-
linear-gradient(180deg, #0F0F0F 0%, #120F0C 100%);
|
| 19 |
-
}
|
| 20 |
-
</style>
|
| 21 |
</head>
|
| 22 |
<body>
|
| 23 |
-
<
|
| 24 |
-
<
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
f"Mixed-strategy NE: P1 plays Row&nbsp;0 with probability "
|
| 142 |
-
f"<strong style='color:{ACCENT}'>p&nbsp;=&nbsp;{p_val:.4f}</strong>, "
|
| 143 |
-
f"P2 plays Col&nbsp;0 with probability "
|
| 144 |
-
f"<strong style='color:{ACCENT}'>q&nbsp;=&nbsp;{q_val:.4f}</strong>.<br>"
|
| 145 |
-
f"Expected payoffs: ({ep1:.3f},&nbsp;{ep2:.3f}).</p>"
|
| 146 |
-
)
|
| 147 |
-
else:
|
| 148 |
-
desc = (
|
| 149 |
-
f"<p style='color:{TEXT_MUT};font-family:IBM Plex Mono,monospace;'>"
|
| 150 |
-
f"⚠ <strong style='color:{TEXT_PRI}'>No pure-strategy Nash equilibrium.</strong> "
|
| 151 |
-
f"Mixed NE is degenerate for this matrix.</p>"
|
| 152 |
-
)
|
| 153 |
-
elif len(eq) == 1:
|
| 154 |
-
r, c = eq[0]
|
| 155 |
-
desc = (
|
| 156 |
-
f"<p style='font-family:IBM Plex Mono,monospace;'>"
|
| 157 |
-
f"✓ <strong style='color:{ACCENT}'>1 Nash equilibrium</strong> "
|
| 158 |
-
f"at position ({r}, {c}) — payoffs ({matrix[r,c,0]}, {matrix[r,c,1]}).</p>"
|
| 159 |
-
)
|
| 160 |
-
else:
|
| 161 |
-
pos_str = ", ".join(f"({r},{c})" for r, c in eq)
|
| 162 |
-
desc = (
|
| 163 |
-
f"<p style='font-family:IBM Plex Mono,monospace;'>"
|
| 164 |
-
f"✓ <strong style='color:{ACCENT}'>{len(eq)} Nash equilibria</strong> "
|
| 165 |
-
f"at positions: {pos_str}.</p>"
|
| 166 |
-
)
|
| 167 |
-
|
| 168 |
-
return (
|
| 169 |
-
f"<div style='text-align:center;padding:1.5rem;'>"
|
| 170 |
-
f"{table_html}"
|
| 171 |
-
f"<div style='margin-top:1rem;'>{desc}</div>"
|
| 172 |
-
f"</div>"
|
| 173 |
-
)
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def classify_matrix(r0c0_p1, r0c0_p2, r0c1_p1, r0c1_p2,
|
| 177 |
-
r1c0_p1, r1c0_p2, r1c1_p1, r1c1_p2):
|
| 178 |
-
payoffs = _parse([r0c0_p1, r0c0_p2, r0c1_p1, r0c1_p2,
|
| 179 |
-
r1c0_p1, r1c0_p2, r1c1_p1, r1c1_p2])
|
| 180 |
-
matrix = _build_matrix(payoffs)
|
| 181 |
-
ne = find_nash_equilibria(matrix)
|
| 182 |
-
props, label = classify_full(matrix, ne)
|
| 183 |
-
|
| 184 |
-
def _bool_badge(v):
|
| 185 |
-
color = ACCENT if v else TEXT_DIM
|
| 186 |
-
symbol = "✓" if v else "✗"
|
| 187 |
-
return (f"<span style='color:{color};font-weight:600;"
|
| 188 |
-
f"font-family:IBM Plex Mono,monospace'>{symbol}</span>")
|
| 189 |
-
|
| 190 |
-
def _row(name, val_html, note=""):
|
| 191 |
-
return (
|
| 192 |
-
f"<tr>"
|
| 193 |
-
f"<td style='padding:6px 14px;color:{TEXT_MUT};font-size:0.82rem;"
|
| 194 |
-
f"font-family:IBM Plex Mono,monospace;border-bottom:1px solid {BORDER_STRONG}'>{name}</td>"
|
| 195 |
-
f"<td style='padding:6px 14px;text-align:center;"
|
| 196 |
-
f"border-bottom:1px solid {BORDER_STRONG}'>{val_html}</td>"
|
| 197 |
-
f"<td style='padding:6px 14px;color:{TEXT_DIM};font-size:0.78rem;"
|
| 198 |
-
f"border-bottom:1px solid {BORDER_STRONG}'>{note}</td>"
|
| 199 |
-
f"</tr>"
|
| 200 |
-
)
|
| 201 |
-
|
| 202 |
-
ne_str = str(ne) if ne else "none"
|
| 203 |
-
nw_str = str(props["ne_welfare"]) if props["ne_welfare"] else "—"
|
| 204 |
-
|
| 205 |
-
prop_rows = [
|
| 206 |
-
_row("P1 has dominant strategy", _bool_badge(props["p1_has_dominant"]),
|
| 207 |
-
"a row weakly best in every column"),
|
| 208 |
-
_row("P2 has dominant strategy", _bool_badge(props["p2_has_dominant"]),
|
| 209 |
-
"a column weakly best in every row"),
|
| 210 |
-
_row("Both dominant", _bool_badge(props["both_dominant"])),
|
| 211 |
-
_row("Zero-sum", _bool_badge(props["is_zero_sum"]),
|
| 212 |
-
"p1+p2 constant across all cells"),
|
| 213 |
-
_row("Symmetric", _bool_badge(props["is_symmetric"]),
|
| 214 |
-
"matrix[r,c,0] == matrix[c,r,1]"),
|
| 215 |
-
_row("NE count",
|
| 216 |
-
f"<span style='color:{TEXT_PRI};font-weight:600;"
|
| 217 |
-
f"font-family:IBM Plex Mono,monospace'>{props['ne_count']}</span>",
|
| 218 |
-
ne_str),
|
| 219 |
-
_row("Any NE Pareto-dominated", _bool_badge(props["has_pareto_dom_ne"]),
|
| 220 |
-
"some other cell beats the NE for both players"),
|
| 221 |
-
_row("All NE Pareto-efficient", _bool_badge(props["all_ne_pareto_eff"])),
|
| 222 |
-
_row("Max social welfare",
|
| 223 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 224 |
-
f"{props['max_welfare']}</span>", "best possible p1+p2"),
|
| 225 |
-
_row("NE welfare",
|
| 226 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 227 |
-
f"{nw_str}</span>"),
|
| 228 |
-
_row("Welfare loss",
|
| 229 |
-
f"<span style='color:{'#C0392B' if props['welfare_loss'] > 0 else '#4CAF82'};"
|
| 230 |
-
f"font-weight:600;font-family:IBM Plex Mono,monospace'>{props['welfare_loss']}</span>",
|
| 231 |
-
"max_welfare − best NE welfare"),
|
| 232 |
-
]
|
| 233 |
-
|
| 234 |
-
if props["ne_count"] > 0:
|
| 235 |
-
mean_diff_str = (f"{props['ne_mean_abs_diff']:.3f}"
|
| 236 |
-
if props["ne_mean_abs_diff"] is not None else "—")
|
| 237 |
-
prop_rows += [
|
| 238 |
-
_row("NE payoffs — P1",
|
| 239 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 240 |
-
f"{props['ne_p1_payoffs']}</span>", "per equilibrium"),
|
| 241 |
-
_row("NE payoffs — P2",
|
| 242 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 243 |
-
f"{props['ne_p2_payoffs']}</span>", "per equilibrium"),
|
| 244 |
-
_row("Payoff diff (P1−P2)",
|
| 245 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 246 |
-
f"{props['ne_payoff_diffs']}</span>", "per equilibrium"),
|
| 247 |
-
_row("Any NE with equal payoffs", _bool_badge(props["ne_has_equal_payoffs"]),
|
| 248 |
-
"P1 == P2 at some NE"),
|
| 249 |
-
_row("Mean |P1−P2| at NE",
|
| 250 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 251 |
-
f"{mean_diff_str}</span>", "payoff asymmetry"),
|
| 252 |
-
]
|
| 253 |
-
|
| 254 |
-
if props["ne_count"] == 0:
|
| 255 |
-
if props["mixed_exists"]:
|
| 256 |
-
prop_rows += [
|
| 257 |
-
_row("Mixed strategy — P1 plays Row 0",
|
| 258 |
-
f"<span style='color:{ACCENT};font-weight:600;"
|
| 259 |
-
f"font-family:IBM Plex Mono,monospace'>p = {props['mixed_p']:.4f}</span>",
|
| 260 |
-
"probability ∈ [0, 1]"),
|
| 261 |
-
_row("Mixed strategy — P2 plays Col 0",
|
| 262 |
-
f"<span style='color:{ACCENT};font-weight:600;"
|
| 263 |
-
f"font-family:IBM Plex Mono,monospace'>q = {props['mixed_q']:.4f}</span>",
|
| 264 |
-
"probability ∈ [0, 1]"),
|
| 265 |
-
_row("Expected payoffs at mixed NE",
|
| 266 |
-
f"<span style='color:{TEXT_PRI};font-family:IBM Plex Mono,monospace'>"
|
| 267 |
-
f"({props['mixed_payoff_p1']:.3f}, {props['mixed_payoff_p2']:.3f})</span>",
|
| 268 |
-
"(P1, P2)"),
|
| 269 |
-
]
|
| 270 |
-
else:
|
| 271 |
-
prop_rows.append(
|
| 272 |
-
_row("Mixed strategy",
|
| 273 |
-
f"<span style='color:{TEXT_DIM}'>none (degenerate)</span>",
|
| 274 |
-
"denominator = 0")
|
| 275 |
-
)
|
| 276 |
-
|
| 277 |
-
props_html = (
|
| 278 |
-
f"<div style='background:{BG_SURFACE};border:1px solid {BORDER_STRONG};"
|
| 279 |
-
f"border-radius:6px;overflow:hidden;margin-top:0.5rem'>"
|
| 280 |
-
f"<table style='width:100%;border-collapse:collapse'>{''.join(prop_rows)}</table></div>"
|
| 281 |
-
)
|
| 282 |
-
|
| 283 |
-
badge_color = GAME_TYPE_COLORS.get(label, TEXT_MUT)
|
| 284 |
-
game_type_html = (
|
| 285 |
-
f"<div style='text-align:center;padding:1.5rem 1rem'>"
|
| 286 |
-
f"<div style='font-family:IBM Plex Mono,monospace;font-size:0.72rem;"
|
| 287 |
-
f"color:{TEXT_DIM};text-transform:uppercase;letter-spacing:0.12em;"
|
| 288 |
-
f"margin-bottom:0.6rem'>Game Type</div>"
|
| 289 |
-
f"<div style='font-family:Space Grotesk,sans-serif;font-weight:700;"
|
| 290 |
-
f"font-size:2rem;color:{badge_color};letter-spacing:-0.01em'>{label}</div>"
|
| 291 |
-
f"</div>"
|
| 292 |
-
)
|
| 293 |
-
|
| 294 |
-
desc = GAME_TYPE_DESCRIPTIONS.get(label, "")
|
| 295 |
-
desc_html = (
|
| 296 |
-
f"<div style='background:{BG_ALT};border-left:3px solid {badge_color};"
|
| 297 |
-
f"padding:1rem 1.2rem;border-radius:0 6px 6px 0;margin-top:0.5rem;"
|
| 298 |
-
f"font-family:IBM Plex Mono,monospace;font-size:0.84rem;"
|
| 299 |
-
f"color:{TEXT_MUT};line-height:1.6'>{desc}</div>"
|
| 300 |
-
) if desc else ""
|
| 301 |
-
|
| 302 |
-
return props_html, game_type_html, desc_html
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
def build_app():
|
| 306 |
-
with gr.Blocks(title="Game Theory Matrix Classifier") as demo:
|
| 307 |
-
gr.HTML("""
|
| 308 |
-
<div style="padding:1.8rem 1rem 0.5rem;border-bottom:1px solid #3D2418;">
|
| 309 |
-
<div style="font-family:'Space Grotesk',sans-serif;font-weight:700;
|
| 310 |
-
font-size:1.5rem;text-transform:uppercase;
|
| 311 |
-
letter-spacing:-0.02em;color:#F0EDE8;">
|
| 312 |
-
Game Theory Matrix Classifier
|
| 313 |
-
</div>
|
| 314 |
-
<div style="font-family:'IBM Plex Mono',monospace;font-size:0.78rem;
|
| 315 |
-
color:#7A7570;margin-top:0.25rem;">
|
| 316 |
-
Compact 2x2 game-theory demo · equilibria, mixed strategies, and classic game labels
|
| 317 |
-
</div>
|
| 318 |
-
</div>
|
| 319 |
-
""")
|
| 320 |
-
|
| 321 |
-
gr.Markdown("""
|
| 322 |
-
This public Space is the lightweight front-facing slice of a larger local research project.
|
| 323 |
-
It focuses on one thing: helping you explore **2x2 payoff matrices** without exposing notebooks,
|
| 324 |
-
write-ups, or large local datasets.
|
| 325 |
-
|
| 326 |
-
What you can do here:
|
| 327 |
-
|
| 328 |
-
1. Enter any 2x2 payoff matrix and find its pure-strategy Nash equilibria
|
| 329 |
-
2. See the mixed-strategy equilibrium when no pure one exists
|
| 330 |
-
3. Classify the matrix into familiar game types such as
|
| 331 |
-
`Prisoner's Dilemma`, `Battle of the Sexes`, `Stag Hunt`, `Chicken`,
|
| 332 |
-
`Coordination`, `Zero-Sum`, and more
|
| 333 |
-
|
| 334 |
-
Use **Tab 01** for fast equilibrium checks. Use **Tab 02** for the fuller structural readout:
|
| 335 |
-
dominant strategies, Pareto efficiency, welfare loss, asymmetry at equilibrium, and the
|
| 336 |
-
game-type label.
|
| 337 |
-
""")
|
| 338 |
-
|
| 339 |
-
with gr.Tabs():
|
| 340 |
-
with gr.Tab("01 / MATRIX EXPLORER"):
|
| 341 |
-
gr.Markdown(
|
| 342 |
-
"> Enter any 2x2 payoff matrix. Each cell takes two numbers: "
|
| 343 |
-
"**Player 1's payoff** and **Player 2's payoff**. Hit **Compute** "
|
| 344 |
-
"to find all pure-strategy Nash equilibria instantly.\n\n"
|
| 345 |
-
"Or load a classic game from the presets."
|
| 346 |
-
)
|
| 347 |
-
|
| 348 |
-
with gr.Row():
|
| 349 |
-
preset_dd = gr.Dropdown(
|
| 350 |
-
choices=list(PRESETS.keys()),
|
| 351 |
-
label="Load a classic game",
|
| 352 |
-
value=None,
|
| 353 |
-
)
|
| 354 |
-
|
| 355 |
-
gr.HTML(
|
| 356 |
-
"<div style='padding:0.4rem 0;color:#4A4540;font-size:0.75rem;"
|
| 357 |
-
"font-family:IBM Plex Mono,monospace;'>"
|
| 358 |
-
"Row labels → Row 0 / Row 1 &nbsp;·&nbsp; "
|
| 359 |
-
"Column labels → Col 0 / Col 1</div>"
|
| 360 |
-
)
|
| 361 |
-
|
| 362 |
-
with gr.Row():
|
| 363 |
-
with gr.Column():
|
| 364 |
-
gr.HTML("<div class='section-num'>TOP ROW</div>")
|
| 365 |
-
with gr.Row():
|
| 366 |
-
r0c0_p1 = gr.Number(label="(0,0) P1", value=3, precision=0)
|
| 367 |
-
r0c0_p2 = gr.Number(label="(0,0) P2", value=3, precision=0)
|
| 368 |
-
with gr.Row():
|
| 369 |
-
r0c1_p1 = gr.Number(label="(0,1) P1", value=0, precision=0)
|
| 370 |
-
r0c1_p2 = gr.Number(label="(0,1) P2", value=5, precision=0)
|
| 371 |
-
with gr.Column():
|
| 372 |
-
gr.HTML("<div class='section-num'>BOTTOM ROW</div>")
|
| 373 |
-
with gr.Row():
|
| 374 |
-
r1c0_p1 = gr.Number(label="(1,0) P1", value=5, precision=0)
|
| 375 |
-
r1c0_p2 = gr.Number(label="(1,0) P2", value=0, precision=0)
|
| 376 |
-
with gr.Row():
|
| 377 |
-
r1c1_p1 = gr.Number(label="(1,1) P1", value=1, precision=0)
|
| 378 |
-
r1c1_p2 = gr.Number(label="(1,1) P2", value=1, precision=0)
|
| 379 |
-
|
| 380 |
-
compute_btn = gr.Button("⚡ Compute Nash Equilibria", variant="primary")
|
| 381 |
-
output_html = gr.HTML()
|
| 382 |
-
|
| 383 |
-
all_inputs = [r0c0_p1, r0c0_p2, r0c1_p1, r0c1_p2,
|
| 384 |
-
r1c0_p1, r1c0_p2, r1c1_p1, r1c1_p2]
|
| 385 |
-
|
| 386 |
-
compute_btn.click(fn=compute_nash, inputs=all_inputs, outputs=output_html)
|
| 387 |
-
preset_dd.change(fn=load_preset, inputs=preset_dd, outputs=all_inputs)
|
| 388 |
-
demo.load(fn=compute_nash, inputs=all_inputs, outputs=output_html)
|
| 389 |
-
|
| 390 |
-
with gr.Tab("02 / CLASSIFICATION"):
|
| 391 |
-
gr.Markdown(
|
| 392 |
-
"> Classify any 2x2 matrix: game type label, boolean structural "
|
| 393 |
-
"properties, Pareto efficiency, welfare loss, and mixed-strategy "
|
| 394 |
-
"equilibrium when no pure NE exists."
|
| 395 |
-
)
|
| 396 |
-
|
| 397 |
-
with gr.Row():
|
| 398 |
-
cl_preset_dd = gr.Dropdown(
|
| 399 |
-
choices=list(PRESETS.keys()),
|
| 400 |
-
label="Load a classic game",
|
| 401 |
-
value="Prisoner's Dilemma",
|
| 402 |
-
)
|
| 403 |
-
|
| 404 |
-
with gr.Row():
|
| 405 |
-
with gr.Column():
|
| 406 |
-
gr.HTML("<div class='section-num'>TOP ROW</div>")
|
| 407 |
-
with gr.Row():
|
| 408 |
-
c2_r0c0_p1 = gr.Number(label="(0,0) P1", value=3, precision=0)
|
| 409 |
-
c2_r0c0_p2 = gr.Number(label="(0,0) P2", value=3, precision=0)
|
| 410 |
-
with gr.Row():
|
| 411 |
-
c2_r0c1_p1 = gr.Number(label="(0,1) P1", value=0, precision=0)
|
| 412 |
-
c2_r0c1_p2 = gr.Number(label="(0,1) P2", value=5, precision=0)
|
| 413 |
-
with gr.Column():
|
| 414 |
-
gr.HTML("<div class='section-num'>BOTTOM ROW</div>")
|
| 415 |
-
with gr.Row():
|
| 416 |
-
c2_r1c0_p1 = gr.Number(label="(1,0) P1", value=5, precision=0)
|
| 417 |
-
c2_r1c0_p2 = gr.Number(label="(1,0) P2", value=0, precision=0)
|
| 418 |
-
with gr.Row():
|
| 419 |
-
c2_r1c1_p1 = gr.Number(label="(1,1) P1", value=1, precision=0)
|
| 420 |
-
c2_r1c1_p2 = gr.Number(label="(1,1) P2", value=1, precision=0)
|
| 421 |
-
|
| 422 |
-
classify_btn = gr.Button("🔍 Classify Game", variant="primary")
|
| 423 |
-
|
| 424 |
-
c2_inputs = [c2_r0c0_p1, c2_r0c0_p2, c2_r0c1_p1, c2_r0c1_p2,
|
| 425 |
-
c2_r1c0_p1, c2_r1c0_p2, c2_r1c1_p1, c2_r1c1_p2]
|
| 426 |
-
|
| 427 |
-
c2_game_type_html = gr.HTML()
|
| 428 |
-
c2_desc_html = gr.HTML()
|
| 429 |
-
c2_props_html = gr.HTML()
|
| 430 |
-
|
| 431 |
-
classify_btn.click(
|
| 432 |
-
fn=classify_matrix,
|
| 433 |
-
inputs=c2_inputs,
|
| 434 |
-
outputs=[c2_props_html, c2_game_type_html, c2_desc_html],
|
| 435 |
-
)
|
| 436 |
-
cl_preset_dd.change(fn=load_preset, inputs=cl_preset_dd, outputs=c2_inputs)
|
| 437 |
-
demo.load(
|
| 438 |
-
fn=classify_matrix,
|
| 439 |
-
inputs=c2_inputs,
|
| 440 |
-
outputs=[c2_props_html, c2_game_type_html, c2_desc_html],
|
| 441 |
-
)
|
| 442 |
-
return demo
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
if __name__ == "__main__":
|
| 446 |
-
app = build_app()
|
| 447 |
-
app.launch(theme=gr.themes.Base(), css=GRADIO_CSS)
|
| 448 |
-
|
| 449 |
-
</gradio-file><gradio-file name="src/__init__.py">
|
| 450 |
-
|
| 451 |
-
</gradio-file><gradio-file name="src/analysis.py">
|
| 452 |
-
"""
|
| 453 |
-
analysis.py — Game type classification for 2-player payoff matrices.
|
| 454 |
-
|
| 455 |
-
Two-layer approach:
|
| 456 |
-
Layer 1 — classify_properties(matrix) → dict of boolean/numeric flags
|
| 457 |
-
Layer 2 — classify_game_type(props, ne_positions) → human-readable string label
|
| 458 |
-
|
| 459 |
-
Both layers work on a single matrix (numpy array, shape (R, C, 2)).
|
| 460 |
-
For bulk classification of large datasets, use classify_batch() which processes
|
| 461 |
-
a numpy batch (N, R, C, 2) in vectorised operations where possible.
|
| 462 |
-
|
| 463 |
-
Named game types (in priority order):
|
| 464 |
-
"Zero-Sum" — payoffs sum to constant in every cell
|
| 465 |
-
"Prisoner's Dilemma" — both dominant strategies, NE is Pareto-dominated
|
| 466 |
-
"Harmony" — both dominant strategies, NE is Pareto-efficient
|
| 467 |
-
"Deadlock" — both dominant strategies, inefficient but not PD
|
| 468 |
-
"Battle of the Sexes" — 2 diagonal NE, players prefer different ones
|
| 469 |
-
"Stag Hunt" — symmetric coordination with a payoff-dominant diagonal
|
| 470 |
-
"Chicken" — symmetric anti-coordination with off-diagonal NE
|
| 471 |
-
"Coordination" — ≥2 NE with coordination structure not covered above
|
| 472 |
-
"Dominant (P1 only)" — only Player 1 has dominant strategy
|
| 473 |
-
"Dominant (P2 only)" — only Player 2 has dominant strategy
|
| 474 |
-
"No Equilibrium" — no pure-strategy NE
|
| 475 |
-
"Other" — everything else
|
| 476 |
-
|
| 477 |
-
Usage
|
| 478 |
-
-----
|
| 479 |
-
from src.analysis import classify_properties, classify_game_type
|
| 480 |
-
from src.matrix_permutations import find_nash_equilibria
|
| 481 |
-
import numpy as np
|
| 482 |
-
|
| 483 |
-
m = np.array([[[3,3],[0,5]],[[5,0],[1,1]]], dtype=np.int32)
|
| 484 |
-
ne = find_nash_equilibria(m)
|
| 485 |
-
props = classify_properties(m, ne)
|
| 486 |
-
label = classify_game_type(props, ne) # → "Prisoner's Dilemma"
|
| 487 |
-
"""
|
| 488 |
-
|
| 489 |
-
from __future__ import annotations
|
| 490 |
-
from typing import Dict, List, Tuple, Any
|
| 491 |
-
import numpy as np
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
# ---------------------------------------------------------------------------
|
| 495 |
-
# Type alias
|
| 496 |
-
# ---------------------------------------------------------------------------
|
| 497 |
-
|
| 498 |
-
Props = Dict[str, Any]
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
# ---------------------------------------------------------------------------
|
| 502 |
-
# Layer 1: Boolean + numeric properties for a single matrix
|
| 503 |
-
# ---------------------------------------------------------------------------
|
| 504 |
-
|
| 505 |
-
def _has_dominant_strategy(matrix: np.ndarray, player: int) -> bool:
|
| 506 |
-
"""
|
| 507 |
-
Return True if `player` (0=row, 1=col) has a weakly dominant strategy.
|
| 508 |
-
|
| 509 |
-
A strategy s* weakly dominates all others if, for every possible opponent
|
| 510 |
-
strategy, s* gives at least as good a payoff as any other choice.
|
| 511 |
-
|
| 512 |
-
For the row player: there exists row r* such that
|
| 513 |
-
matrix[r*, c, 0] >= matrix[r, c, 0] for ALL r, c
|
| 514 |
-
|
| 515 |
-
For the column player: there exists col c* such that
|
| 516 |
-
matrix[r, c*, 1] >= matrix[r, c, 1] for ALL r, c
|
| 517 |
-
"""
|
| 518 |
-
R, C, _ = matrix.shape
|
| 519 |
-
payoffs = matrix[:, :, player] # (R, C)
|
| 520 |
-
|
| 521 |
-
if player == 0: # row player — look for a dominant row
|
| 522 |
-
for r_star in range(R):
|
| 523 |
-
if np.all(payoffs[r_star, :] >= payoffs):
|
| 524 |
-
return True
|
| 525 |
-
else: # col player — look for a dominant column
|
| 526 |
-
for c_star in range(C):
|
| 527 |
-
if np.all(payoffs[:, c_star][:, None] >= payoffs):
|
| 528 |
-
return True
|
| 529 |
-
return False
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
def _pareto_dominated(cell_r: int, cell_c: int, matrix: np.ndarray) -> bool:
|
| 533 |
-
"""
|
| 534 |
-
Return True if cell (cell_r, cell_c) is Pareto-dominated by any other cell.
|
| 535 |
-
|
| 536 |
-
A cell X is Pareto-dominated by cell Y if:
|
| 537 |
-
Y gives at least as much to both players, and strictly more to at least one.
|
| 538 |
-
"""
|
| 539 |
-
p1_here = int(matrix[cell_r, cell_c, 0])
|
| 540 |
-
p2_here = int(matrix[cell_r, cell_c, 1])
|
| 541 |
-
R, C, _ = matrix.shape
|
| 542 |
-
|
| 543 |
-
for r in range(R):
|
| 544 |
-
for c in range(C):
|
| 545 |
-
if r == cell_r and c == cell_c:
|
| 546 |
-
continue
|
| 547 |
-
p1_there = int(matrix[r, c, 0])
|
| 548 |
-
p2_there = int(matrix[r, c, 1])
|
| 549 |
-
if p1_there >= p1_here and p2_there >= p2_here:
|
| 550 |
-
if p1_there > p1_here or p2_there > p2_here:
|
| 551 |
-
return True
|
| 552 |
-
return False
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
def _compute_mixed_strategy_2x2(matrix: np.ndarray) -> Dict[str, Any]:
|
| 556 |
-
"""
|
| 557 |
-
Compute the mixed-strategy Nash equilibrium for a 2×2 payoff matrix.
|
| 558 |
-
|
| 559 |
-
Layout:
|
| 560 |
-
Row 0: [(a, e), (b, f)] → matrix[0,0]=(a,e), matrix[0,1]=(b,f)
|
| 561 |
-
Row 1: [(c, g), (d, h)] → matrix[1,0]=(c,g), matrix[1,1]=(d,h)
|
| 562 |
-
|
| 563 |
-
P1 mixes with probability p (plays Row 0), making P2 indifferent:
|
| 564 |
-
p·e + (1−p)·g = p·f + (1−p)·h → p = (h−g) / (e−g−f+h)
|
| 565 |
-
|
| 566 |
-
P2 mixes with probability q (plays Col 0), making P1 indifferent:
|
| 567 |
-
q·a + (1−q)·b = q·c + (1−q)·d → q = (d−b) / (a−b−c+d)
|
| 568 |
-
|
| 569 |
-
Returns a dict with keys:
|
| 570 |
-
mixed_exists bool
|
| 571 |
-
mixed_p float | None P1 plays Row 0 with this probability
|
| 572 |
-
mixed_q float | None P2 plays Col 0 with this probability
|
| 573 |
-
mixed_payoff_p1 float | None P1 expected payoff at mixed NE
|
| 574 |
-
mixed_payoff_p2 float | None P2 expected payoff at mixed NE
|
| 575 |
-
"""
|
| 576 |
-
_null = {"mixed_exists": False, "mixed_p": None, "mixed_q": None,
|
| 577 |
-
"mixed_payoff_p1": None, "mixed_payoff_p2": None}
|
| 578 |
-
|
| 579 |
-
if matrix.shape != (2, 2, 2):
|
| 580 |
-
return _null
|
| 581 |
-
|
| 582 |
-
a, e = float(matrix[0, 0, 0]), float(matrix[0, 0, 1])
|
| 583 |
-
b, f = float(matrix[0, 1, 0]), float(matrix[0, 1, 1])
|
| 584 |
-
c, g = float(matrix[1, 0, 0]), float(matrix[1, 0, 1])
|
| 585 |
-
d, h = float(matrix[1, 1, 0]), float(matrix[1, 1, 1])
|
| 586 |
-
|
| 587 |
-
denom_p = e - g - f + h # denominator for p
|
| 588 |
-
denom_q = a - b - c + d # denominator for q
|
| 589 |
-
|
| 590 |
-
if denom_p == 0.0 or denom_q == 0.0:
|
| 591 |
-
return _null
|
| 592 |
-
|
| 593 |
-
p = (h - g) / denom_p
|
| 594 |
-
q = (d - b) / denom_q
|
| 595 |
-
|
| 596 |
-
eps = 1e-9
|
| 597 |
-
if not (-eps <= p <= 1.0 + eps and -eps <= q <= 1.0 + eps):
|
| 598 |
-
return _null
|
| 599 |
-
|
| 600 |
-
# Clamp to [0, 1] to absorb floating-point edge cases
|
| 601 |
-
p = max(0.0, min(1.0, p))
|
| 602 |
-
q = max(0.0, min(1.0, q))
|
| 603 |
-
|
| 604 |
-
ep1 = round(q * a + (1.0 - q) * b, 6)
|
| 605 |
-
ep2 = round(p * e + (1.0 - p) * g, 6)
|
| 606 |
-
|
| 607 |
-
return {
|
| 608 |
-
"mixed_exists": True,
|
| 609 |
-
"mixed_p": round(p, 6),
|
| 610 |
-
"mixed_q": round(q, 6),
|
| 611 |
-
"mixed_payoff_p1": ep1,
|
| 612 |
-
"mixed_payoff_p2": ep2,
|
| 613 |
-
}
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
def _ne_payoff_stats(
|
| 617 |
-
matrix: np.ndarray,
|
| 618 |
-
ne_positions: List[Tuple[int, int]],
|
| 619 |
-
) -> Dict[str, Any]:
|
| 620 |
-
"""
|
| 621 |
-
Compute per-NE payoff statistics for a single matrix.
|
| 622 |
-
|
| 623 |
-
Returns a dict with keys:
|
| 624 |
-
ne_p1_payoffs list[int] — P1 payoff at each NE
|
| 625 |
-
ne_p2_payoffs list[int] — P2 payoff at each NE
|
| 626 |
-
ne_payoff_diffs list[int] — (P1−P2) at each NE
|
| 627 |
-
ne_has_equal_payoffs bool — True if any NE has P1 == P2
|
| 628 |
-
ne_mean_abs_diff float|None — mean |P1−P2| across all NE; None if no NE
|
| 629 |
-
"""
|
| 630 |
-
if not ne_positions:
|
| 631 |
-
return {
|
| 632 |
-
"ne_p1_payoffs": [],
|
| 633 |
-
"ne_p2_payoffs": [],
|
| 634 |
-
"ne_payoff_diffs": [],
|
| 635 |
-
"ne_has_equal_payoffs": False,
|
| 636 |
-
"ne_mean_abs_diff": None,
|
| 637 |
-
}
|
| 638 |
-
|
| 639 |
-
p1_payoffs = [int(matrix[r, c, 0]) for r, c in ne_positions]
|
| 640 |
-
p2_payoffs = [int(matrix[r, c, 1]) for r, c in ne_positions]
|
| 641 |
-
diffs = [p1 - p2 for p1, p2 in zip(p1_payoffs, p2_payoffs)]
|
| 642 |
-
|
| 643 |
-
return {
|
| 644 |
-
"ne_p1_payoffs": p1_payoffs,
|
| 645 |
-
"ne_p2_payoffs": p2_payoffs,
|
| 646 |
-
"ne_payoff_diffs": diffs,
|
| 647 |
-
"ne_has_equal_payoffs": any(d == 0 for d in diffs),
|
| 648 |
-
"ne_mean_abs_diff": sum(abs(d) for d in diffs) / len(diffs),
|
| 649 |
-
}
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
def classify_properties(
|
| 653 |
-
matrix: np.ndarray,
|
| 654 |
-
ne_positions: List[Tuple[int, int]],
|
| 655 |
-
) -> Props:
|
| 656 |
-
"""
|
| 657 |
-
Compute a dictionary of structural properties for a single payoff matrix.
|
| 658 |
-
|
| 659 |
-
Parameters
|
| 660 |
-
----------
|
| 661 |
-
matrix : np.ndarray, shape (R, C, 2)
|
| 662 |
-
ne_positions : list of (row, col) Nash equilibria (from find_nash_equilibria)
|
| 663 |
-
|
| 664 |
-
Returns
|
| 665 |
-
-------
|
| 666 |
-
props : dict with the following keys:
|
| 667 |
-
|
| 668 |
-
p1_has_dominant bool — P1 has a weakly dominant strategy
|
| 669 |
-
p2_has_dominant bool — P2 has a weakly dominant strategy
|
| 670 |
-
both_dominant bool — both players have dominant strategies
|
| 671 |
-
is_zero_sum bool — all cells: p1+p2 == constant
|
| 672 |
-
is_symmetric bool — matrix[r,c,0] == matrix[c,r,1] (requires R==C)
|
| 673 |
-
ne_count int — number of pure-strategy NE
|
| 674 |
-
has_pareto_dom_ne bool — any NE is Pareto-dominated by another cell
|
| 675 |
-
all_ne_pareto_eff bool — no NE is Pareto-dominated
|
| 676 |
-
max_welfare int — max(p1+p2) over all cells
|
| 677 |
-
ne_welfare list — p1+p2 at each NE position
|
| 678 |
-
welfare_loss int — max_welfare − max(ne_welfare); 0 means NE is optimal
|
| 679 |
-
|
| 680 |
-
mixed_exists bool — True if a valid mixed-strategy NE exists
|
| 681 |
-
(only computed for 2×2 matrices with 0 pure NE)
|
| 682 |
-
mixed_p float|None — P1 plays Row 0 with this probability
|
| 683 |
-
mixed_q float|None — P2 plays Col 0 with this probability
|
| 684 |
-
mixed_payoff_p1 float|None — P1 expected payoff at mixed NE
|
| 685 |
-
mixed_payoff_p2 float|None — P2 expected payoff at mixed NE
|
| 686 |
-
|
| 687 |
-
ne_p1_payoffs list[int] — P1 payoff at each NE (empty if no pure NE)
|
| 688 |
-
ne_p2_payoffs list[int] — P2 payoff at each NE
|
| 689 |
-
ne_payoff_diffs list[int] — (P1−P2) at each NE
|
| 690 |
-
ne_has_equal_payoffs bool — True if any NE has P1 == P2
|
| 691 |
-
ne_mean_abs_diff float|None — mean |P1−P2| across all NE; None if no pure NE
|
| 692 |
-
"""
|
| 693 |
-
R, C, _ = matrix.shape
|
| 694 |
-
p1 = matrix[:, :, 0]
|
| 695 |
-
p2 = matrix[:, :, 1]
|
| 696 |
-
welfare = p1 + p2 # (R, C)
|
| 697 |
-
|
| 698 |
-
# Dominant strategies
|
| 699 |
-
p1_dom = _has_dominant_strategy(matrix, 0)
|
| 700 |
-
p2_dom = _has_dominant_strategy(matrix, 1)
|
| 701 |
-
|
| 702 |
-
# Zero-sum: all payoff sums identical
|
| 703 |
-
sums = (p1 + p2).ravel()
|
| 704 |
-
is_zero_sum = bool(np.all(sums == sums[0]))
|
| 705 |
-
|
| 706 |
-
# Symmetry: matrix[r,c,0] == matrix[c,r,1] (only meaningful for square)
|
| 707 |
-
if R == C:
|
| 708 |
-
is_sym = all(
|
| 709 |
-
int(matrix[r, c, 0]) == int(matrix[c, r, 1])
|
| 710 |
-
for r in range(R) for c in range(C)
|
| 711 |
-
)
|
| 712 |
-
else:
|
| 713 |
-
is_sym = False
|
| 714 |
-
|
| 715 |
-
# NE welfare
|
| 716 |
-
ne_w = [int(welfare[r, c]) for r, c in ne_positions]
|
| 717 |
-
max_w = int(welfare.max())
|
| 718 |
-
max_ne_w = max(ne_w) if ne_w else 0
|
| 719 |
-
w_loss = max_w - max_ne_w
|
| 720 |
-
|
| 721 |
-
# Pareto efficiency of each NE
|
| 722 |
-
ne_pareto_dom = [_pareto_dominated(r, c, matrix) for r, c in ne_positions]
|
| 723 |
-
has_pd_ne = any(ne_pareto_dom)
|
| 724 |
-
all_pe_ne = not has_pd_ne
|
| 725 |
-
|
| 726 |
-
# Mixed-strategy equilibrium — 2×2 matrices with 0 pure NE only
|
| 727 |
-
if R == 2 and C == 2 and len(ne_positions) == 0:
|
| 728 |
-
mixed = _compute_mixed_strategy_2x2(matrix)
|
| 729 |
-
else:
|
| 730 |
-
mixed = {"mixed_exists": False, "mixed_p": None, "mixed_q": None,
|
| 731 |
-
"mixed_payoff_p1": None, "mixed_payoff_p2": None}
|
| 732 |
-
|
| 733 |
-
# Payoff asymmetry at Nash equilibria
|
| 734 |
-
asym = _ne_payoff_stats(matrix, ne_positions)
|
| 735 |
-
|
| 736 |
-
return {
|
| 737 |
-
"p1_has_dominant": p1_dom,
|
| 738 |
-
"p2_has_dominant": p2_dom,
|
| 739 |
-
"both_dominant": p1_dom and p2_dom,
|
| 740 |
-
"is_zero_sum": is_zero_sum,
|
| 741 |
-
"is_symmetric": is_sym,
|
| 742 |
-
"ne_count": len(ne_positions),
|
| 743 |
-
"has_pareto_dom_ne": has_pd_ne,
|
| 744 |
-
"all_ne_pareto_eff": all_pe_ne,
|
| 745 |
-
"max_welfare": max_w,
|
| 746 |
-
"ne_welfare": ne_w,
|
| 747 |
-
"welfare_loss": w_loss,
|
| 748 |
-
# Mixed strategy
|
| 749 |
-
"mixed_exists": mixed["mixed_exists"],
|
| 750 |
-
"mixed_p": mixed["mixed_p"],
|
| 751 |
-
"mixed_q": mixed["mixed_q"],
|
| 752 |
-
"mixed_payoff_p1": mixed["mixed_payoff_p1"],
|
| 753 |
-
"mixed_payoff_p2": mixed["mixed_payoff_p2"],
|
| 754 |
-
# Payoff asymmetry
|
| 755 |
-
"ne_p1_payoffs": asym["ne_p1_payoffs"],
|
| 756 |
-
"ne_p2_payoffs": asym["ne_p2_payoffs"],
|
| 757 |
-
"ne_payoff_diffs": asym["ne_payoff_diffs"],
|
| 758 |
-
"ne_has_equal_payoffs": asym["ne_has_equal_payoffs"],
|
| 759 |
-
"ne_mean_abs_diff": asym["ne_mean_abs_diff"],
|
| 760 |
-
}
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
# ---------------------------------------------------------------------------
|
| 764 |
-
# Layer 2: Named game type (derived from properties)
|
| 765 |
-
# ---------------------------------------------------------------------------
|
| 766 |
-
|
| 767 |
-
# Plain-English descriptions keyed by type label
|
| 768 |
-
GAME_TYPE_DESCRIPTIONS: Dict[str, str] = {
|
| 769 |
-
"Zero-Sum": (
|
| 770 |
-
"A zero-sum game: one player's gain is exactly the other's loss. "
|
| 771 |
-
"The total welfare is constant across all outcomes. "
|
| 772 |
-
"Classic examples: chess, poker, matching pennies."
|
| 773 |
-
),
|
| 774 |
-
"Prisoner's Dilemma": (
|
| 775 |
-
"A social dilemma: both players have a dominant strategy (defect), "
|
| 776 |
-
"but the Nash equilibrium leaves both worse off than if they had "
|
| 777 |
-
"cooperated. Rational individual behaviour produces a collectively "
|
| 778 |
-
"suboptimal result. Welfare loss > 0."
|
| 779 |
-
),
|
| 780 |
-
"Harmony": (
|
| 781 |
-
"A harmony game: both players have dominant strategies AND the "
|
| 782 |
-
"Nash equilibrium is Pareto-efficient. Rational self-interest "
|
| 783 |
-
"happens to align with the socially optimal outcome — no dilemma."
|
| 784 |
-
),
|
| 785 |
-
"Deadlock": (
|
| 786 |
-
"Both players have dominant strategies leading to an equilibrium, "
|
| 787 |
-
"but unlike the Prisoner's Dilemma the cooperative outcome is not "
|
| 788 |
-
"better for both. Mutual defection is both rational and efficient."
|
| 789 |
-
),
|
| 790 |
-
"Battle of the Sexes": (
|
| 791 |
-
"An asymmetric coordination game with two diagonal equilibria. "
|
| 792 |
-
"Both players want to coordinate, but each prefers a different "
|
| 793 |
-
"equilibrium, so the main challenge is choosing which outcome to meet at."
|
| 794 |
-
),
|
| 795 |
-
"Stag Hunt": (
|
| 796 |
-
"A symmetric coordination game with one high-reward cooperative "
|
| 797 |
-
"equilibrium and one safer fallback equilibrium. Trust matters because "
|
| 798 |
-
"failing to coordinate can be costly."
|
| 799 |
-
),
|
| 800 |
-
"Chicken": (
|
| 801 |
-
"A symmetric anti-coordination game with off-diagonal equilibria. "
|
| 802 |
-
"Each player wants the other side to yield, creating brinkmanship "
|
| 803 |
-
"instead of stable mutual cooperation."
|
| 804 |
-
),
|
| 805 |
-
"Coordination": (
|
| 806 |
-
"A coordination-style game: multiple Nash equilibria exist and the "
|
| 807 |
-
"main strategic problem is choosing which stable outcome to coordinate on."
|
| 808 |
-
),
|
| 809 |
-
"Dominant (P1 only)": (
|
| 810 |
-
"Only Player 1 has a dominant strategy. Player 2's best response "
|
| 811 |
-
"depends on what Player 1 does, but P1 always plays the same way."
|
| 812 |
-
),
|
| 813 |
-
"Dominant (P2 only)": (
|
| 814 |
-
"Only Player 2 has a dominant strategy. Player 1's best response "
|
| 815 |
-
"depends on what Player 2 does, but P2 always plays the same way."
|
| 816 |
-
),
|
| 817 |
-
"No Equilibrium": (
|
| 818 |
-
"No pure-strategy Nash equilibrium exists. Neither player has a "
|
| 819 |
-
"stable resting point; best responses cycle. A mixed-strategy "
|
| 820 |
-
"equilibrium always exists (Nash's theorem) and is computed for "
|
| 821 |
-
"2×2 matrices — see the mixed-strategy properties below."
|
| 822 |
-
),
|
| 823 |
-
"Other": (
|
| 824 |
-
"A game that doesn't fit neatly into the classic taxonomy. "
|
| 825 |
-
"Neither player has a dominant strategy and there is at least one "
|
| 826 |
-
"pure-strategy Nash equilibrium."
|
| 827 |
-
),
|
| 828 |
-
}
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
def _is_diagonal_pair(ne_positions: List[Tuple[int, int]]) -> bool:
|
| 832 |
-
return set(ne_positions) == {(0, 0), (1, 1)}
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
def _is_off_diagonal_pair(ne_positions: List[Tuple[int, int]]) -> bool:
|
| 836 |
-
return set(ne_positions) == {(0, 1), (1, 0)}
|
| 837 |
-
|
| 838 |
-
|
| 839 |
-
def classify_game_type(
|
| 840 |
-
props: Props,
|
| 841 |
-
ne_positions: List[Tuple[int, int]],
|
| 842 |
-
matrix: np.ndarray | None = None,
|
| 843 |
-
) -> str:
|
| 844 |
-
"""
|
| 845 |
-
Assign a named game type label based on the properties dict.
|
| 846 |
-
|
| 847 |
-
Checked in strict priority order — the first matching rule wins.
|
| 848 |
-
"""
|
| 849 |
-
if props["is_zero_sum"]:
|
| 850 |
-
return "Zero-Sum"
|
| 851 |
-
|
| 852 |
-
if props["both_dominant"]:
|
| 853 |
-
# True PD: the NE is Pareto-suboptimal AND achieves less than max welfare.
|
| 854 |
-
# (welfare_loss > 0 rules out the edge case where one NE Pareto-dominates
|
| 855 |
-
# another NE but the dominant NE still achieves max social welfare.)
|
| 856 |
-
if props["has_pareto_dom_ne"] and props["welfare_loss"] > 0:
|
| 857 |
-
return "Prisoner's Dilemma"
|
| 858 |
-
elif props["welfare_loss"] == 0:
|
| 859 |
-
return "Harmony"
|
| 860 |
-
else:
|
| 861 |
-
return "Deadlock"
|
| 862 |
-
|
| 863 |
-
if matrix is not None and len(ne_positions) == 2 and _is_diagonal_pair(ne_positions):
|
| 864 |
-
tl = matrix[0, 0]
|
| 865 |
-
br = matrix[1, 1]
|
| 866 |
-
|
| 867 |
-
p1_prefers_tl = int(tl[0]) > int(br[0])
|
| 868 |
-
p1_prefers_br = int(br[0]) > int(tl[0])
|
| 869 |
-
p2_prefers_tl = int(tl[1]) > int(br[1])
|
| 870 |
-
p2_prefers_br = int(br[1]) > int(tl[1])
|
| 871 |
-
|
| 872 |
-
if (p1_prefers_tl and p2_prefers_br) or (p1_prefers_br and p2_prefers_tl):
|
| 873 |
-
return "Battle of the Sexes"
|
| 874 |
-
|
| 875 |
-
if props["is_symmetric"]:
|
| 876 |
-
tl_welfare = int(tl[0] + tl[1])
|
| 877 |
-
br_welfare = int(br[0] + br[1])
|
| 878 |
-
if tl_welfare != br_welfare:
|
| 879 |
-
return "Stag Hunt"
|
| 880 |
-
|
| 881 |
-
return "Coordination"
|
| 882 |
-
|
| 883 |
-
if matrix is not None and len(ne_positions) == 2 and _is_off_diagonal_pair(ne_positions):
|
| 884 |
-
if props["is_symmetric"]:
|
| 885 |
-
return "Chicken"
|
| 886 |
-
|
| 887 |
-
if len(ne_positions) >= 2 and props["is_symmetric"]:
|
| 888 |
-
return "Coordination"
|
| 889 |
-
|
| 890 |
-
if props["p1_has_dominant"] and not props["p2_has_dominant"]:
|
| 891 |
-
return "Dominant (P1 only)"
|
| 892 |
-
|
| 893 |
-
if props["p2_has_dominant"] and not props["p1_has_dominant"]:
|
| 894 |
-
return "Dominant (P2 only)"
|
| 895 |
-
|
| 896 |
-
if props["ne_count"] == 0:
|
| 897 |
-
return "No Equilibrium"
|
| 898 |
-
|
| 899 |
-
return "Other"
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
def classify_full(
|
| 903 |
-
matrix: np.ndarray,
|
| 904 |
-
ne_positions: List[Tuple[int, int]],
|
| 905 |
-
) -> Tuple[Props, str]:
|
| 906 |
-
"""
|
| 907 |
-
Convenience wrapper: compute both layers and return (props, game_type).
|
| 908 |
-
"""
|
| 909 |
-
props = classify_properties(matrix, ne_positions)
|
| 910 |
-
label = classify_game_type(props, ne_positions, matrix)
|
| 911 |
-
return props, label
|
| 912 |
-
|
| 913 |
-
|
| 914 |
-
# ---------------------------------------------------------------------------
|
| 915 |
-
# Bulk classification (used by enrich_datasets.py)
|
| 916 |
-
# ---------------------------------------------------------------------------
|
| 917 |
-
|
| 918 |
-
def classify_rows_batch(
|
| 919 |
-
matrices_flat: np.ndarray,
|
| 920 |
-
ne_positions_list: List[List[Tuple[int, int]]],
|
| 921 |
-
rows: int,
|
| 922 |
-
cols: int,
|
| 923 |
-
) -> List[Dict[str, Any]]:
|
| 924 |
-
"""
|
| 925 |
-
Classify a list of matrices given their pre-computed NE positions.
|
| 926 |
-
|
| 927 |
-
Parameters
|
| 928 |
-
----------
|
| 929 |
-
matrices_flat : (N, rows*cols*2) array of int — payoff values flat
|
| 930 |
-
ne_positions_list : list of length N, each element a list of (r,c) NE positions
|
| 931 |
-
rows, cols : matrix dimensions
|
| 932 |
-
|
| 933 |
-
Returns
|
| 934 |
-
-------
|
| 935 |
-
List of dicts, one per matrix, with keys:
|
| 936 |
-
p1_has_dominant, p2_has_dominant, both_dominant, is_zero_sum,
|
| 937 |
-
is_symmetric, has_pareto_dom_ne, all_ne_pareto_eff,
|
| 938 |
-
max_welfare, ne_welfare, welfare_loss,
|
| 939 |
-
mixed_exists, mixed_p, mixed_q, mixed_payoff_p1, mixed_payoff_p2,
|
| 940 |
-
ne_p1_payoffs, ne_p2_payoffs, ne_payoff_diffs,
|
| 941 |
-
ne_has_equal_payoffs, ne_mean_abs_diff,
|
| 942 |
-
game_type
|
| 943 |
-
"""
|
| 944 |
-
results = []
|
| 945 |
-
for i, flat in enumerate(matrices_flat):
|
| 946 |
-
matrix = flat.reshape(rows, cols, 2)
|
| 947 |
-
ne = ne_positions_list[i]
|
| 948 |
-
props, label = classify_full(matrix, ne)
|
| 949 |
-
results.append({**props, "game_type": label})
|
| 950 |
-
return results
|
| 951 |
-
|
| 952 |
-
</gradio-file><gradio-file name="src/matrix_permutations.py">
|
| 953 |
-
import itertools
|
| 954 |
-
import numpy as np
|
| 955 |
-
import csv
|
| 956 |
-
from typing import List, Tuple, Iterator, Generator
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
# ---------------------------------------------------------------------------
|
| 960 |
-
# Core n×m generation
|
| 961 |
-
# ---------------------------------------------------------------------------
|
| 962 |
-
|
| 963 |
-
def generate_dual_matrices(rows: int, cols: int, min_val: int = 0, max_val: int = 5) -> List[np.ndarray]:
|
| 964 |
-
"""
|
| 965 |
-
Generate ALL possible n×m matrices where every cell holds a (player1, player2)
|
| 966 |
-
payoff pair. Both payoffs range from min_val to max_val inclusive.
|
| 967 |
-
|
| 968 |
-
Returns a list of numpy arrays with shape (rows, cols, 2).
|
| 969 |
-
|
| 970 |
-
Use generate_dual_matrices_iter() instead when the full list would be too
|
| 971 |
-
large to hold in RAM (e.g. 0-10 range with 214M matrices).
|
| 972 |
-
"""
|
| 973 |
-
values = range(min_val, max_val + 1)
|
| 974 |
-
position_pairs = list(itertools.product(values, values))
|
| 975 |
-
num_cells = rows * cols
|
| 976 |
-
|
| 977 |
-
matrices = []
|
| 978 |
-
for perm in itertools.product(position_pairs, repeat=num_cells):
|
| 979 |
-
matrix = np.array(perm, dtype=np.int32).reshape(rows, cols, 2)
|
| 980 |
-
matrices.append(matrix)
|
| 981 |
-
return matrices
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
def generate_dual_matrices_iter(
|
| 985 |
-
rows: int, cols: int, min_val: int = 0, max_val: int = 5
|
| 986 |
-
) -> Iterator[np.ndarray]:
|
| 987 |
-
"""
|
| 988 |
-
Generator version of generate_dual_matrices().
|
| 989 |
-
Yields one (rows, cols, 2) numpy array at a time — never holds the full
|
| 990 |
-
dataset in memory. Use this for large ranges (e.g. 0–10).
|
| 991 |
-
"""
|
| 992 |
-
values = range(min_val, max_val + 1)
|
| 993 |
-
position_pairs = list(itertools.product(values, values))
|
| 994 |
-
num_cells = rows * cols
|
| 995 |
-
|
| 996 |
-
for perm in itertools.product(position_pairs, repeat=num_cells):
|
| 997 |
-
yield np.array(perm, dtype=np.int32).reshape(rows, cols, 2)
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
def generate_dual_matrices_batched(
|
| 1001 |
-
rows: int,
|
| 1002 |
-
cols: int,
|
| 1003 |
-
min_val: int = 0,
|
| 1004 |
-
max_val: int = 5,
|
| 1005 |
-
batch_size: int = 50_000,
|
| 1006 |
-
) -> Generator[np.ndarray, None, None]:
|
| 1007 |
-
"""
|
| 1008 |
-
Yields batches of matrices as a single numpy array of shape (B, rows, cols, 2).
|
| 1009 |
-
|
| 1010 |
-
B = batch_size for all batches except possibly the last one.
|
| 1011 |
-
This is the most efficient input form for find_nash_batch_vectorized().
|
| 1012 |
-
|
| 1013 |
-
Example
|
| 1014 |
-
-------
|
| 1015 |
-
for batch in generate_dual_matrices_batched(2, 2, 0, 5, batch_size=50_000):
|
| 1016 |
-
is_ne, counts = find_nash_batch_vectorized(batch)
|
| 1017 |
-
# batch.shape == (50_000, 2, 2, 2) for all but the last chunk
|
| 1018 |
-
"""
|
| 1019 |
-
values = range(min_val, max_val + 1)
|
| 1020 |
-
position_pairs = list(itertools.product(values, values))
|
| 1021 |
-
num_cells = rows * cols
|
| 1022 |
-
buf: list[np.ndarray] = []
|
| 1023 |
-
|
| 1024 |
-
for perm in itertools.product(position_pairs, repeat=num_cells):
|
| 1025 |
-
buf.append(np.array(perm, dtype=np.int32))
|
| 1026 |
-
if len(buf) == batch_size:
|
| 1027 |
-
yield np.stack(buf).reshape(batch_size, rows, cols, 2)
|
| 1028 |
-
buf = []
|
| 1029 |
-
|
| 1030 |
-
if buf:
|
| 1031 |
-
n = len(buf)
|
| 1032 |
-
yield np.stack(buf).reshape(n, rows, cols, 2)
|
| 1033 |
-
|
| 1034 |
-
|
| 1035 |
-
# ---------------------------------------------------------------------------
|
| 1036 |
-
# Nash equilibrium analysis — single matrix (Python loops)
|
| 1037 |
-
# ---------------------------------------------------------------------------
|
| 1038 |
-
|
| 1039 |
-
def check_nash_equilibrium(matrix: np.ndarray, position: Tuple[int, int]) -> bool:
|
| 1040 |
-
"""
|
| 1041 |
-
Return True if (row, col) is a pure-strategy Nash equilibrium.
|
| 1042 |
-
|
| 1043 |
-
A cell is a NE when:
|
| 1044 |
-
- The row player cannot improve their payoff by switching to any other row
|
| 1045 |
-
(keeping the column fixed).
|
| 1046 |
-
- The column player cannot improve their payoff by switching to any other
|
| 1047 |
-
column (keeping the row fixed).
|
| 1048 |
-
|
| 1049 |
-
Works for any n×m matrix shape.
|
| 1050 |
-
"""
|
| 1051 |
-
row, col = position
|
| 1052 |
-
current_row_payoff = int(matrix[row, col, 0])
|
| 1053 |
-
current_col_payoff = int(matrix[row, col, 1])
|
| 1054 |
-
|
| 1055 |
-
# Can the row player do better by moving to a different row?
|
| 1056 |
-
for other_row in range(matrix.shape[0]):
|
| 1057 |
-
if other_row != row and int(matrix[other_row, col, 0]) > current_row_payoff:
|
| 1058 |
-
return False
|
| 1059 |
-
|
| 1060 |
-
# Can the column player do better by moving to a different column?
|
| 1061 |
-
for other_col in range(matrix.shape[1]):
|
| 1062 |
-
if other_col != col and int(matrix[row, other_col, 1]) > current_col_payoff:
|
| 1063 |
-
return False
|
| 1064 |
-
|
| 1065 |
-
return True
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
def find_nash_equilibria(matrix: np.ndarray) -> List[Tuple[int, int]]:
|
| 1069 |
-
"""
|
| 1070 |
-
Find all pure-strategy Nash equilibria in an n×m payoff matrix.
|
| 1071 |
-
Returns a (possibly empty) list of (row, col) positions.
|
| 1072 |
-
"""
|
| 1073 |
-
equilibria = []
|
| 1074 |
-
for row in range(matrix.shape[0]):
|
| 1075 |
-
for col in range(matrix.shape[1]):
|
| 1076 |
-
if check_nash_equilibrium(matrix, (row, col)):
|
| 1077 |
-
equilibria.append((row, col))
|
| 1078 |
-
return equilibria
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
# ---------------------------------------------------------------------------
|
| 1082 |
-
# Nash equilibrium analysis — vectorised batch (numpy, no Python loops)
|
| 1083 |
-
# ---------------------------------------------------------------------------
|
| 1084 |
-
|
| 1085 |
-
def find_nash_batch_vectorized(
|
| 1086 |
-
batch: np.ndarray,
|
| 1087 |
-
) -> Tuple[np.ndarray, np.ndarray]:
|
| 1088 |
-
"""
|
| 1089 |
-
Find pure-strategy Nash equilibria for a BATCH of matrices in one numpy call.
|
| 1090 |
-
|
| 1091 |
-
Parameters
|
| 1092 |
-
----------
|
| 1093 |
-
batch : np.ndarray, shape (N, rows, cols, 2)
|
| 1094 |
-
N matrices, each of shape (rows, cols, 2) where the last axis is
|
| 1095 |
-
[player1_payoff, player2_payoff].
|
| 1096 |
-
|
| 1097 |
-
Returns
|
| 1098 |
-
-------
|
| 1099 |
-
is_ne : np.ndarray, shape (N, rows, cols), dtype bool
|
| 1100 |
-
True wherever a cell is a Nash equilibrium.
|
| 1101 |
-
ne_counts : np.ndarray, shape (N,), dtype int
|
| 1102 |
-
Number of Nash equilibria per matrix.
|
| 1103 |
-
|
| 1104 |
-
How it works
|
| 1105 |
-
------------
|
| 1106 |
-
A cell (r, c) in matrix n is a NE when:
|
| 1107 |
-
• P1 cannot strictly improve by switching rows:
|
| 1108 |
-
p1[n, r, c] == max over r' of p1[n, r', c]
|
| 1109 |
-
• P2 cannot strictly improve by switching columns:
|
| 1110 |
-
p2[n, r, c] == max over c' of p2[n, r, c']
|
| 1111 |
-
|
| 1112 |
-
Both conditions are expressed as a single element-wise equality after
|
| 1113 |
-
broadcasting the per-column and per-row maxima — no Python loops needed.
|
| 1114 |
-
|
| 1115 |
-
This is equivalent to find_nash_equilibria() but ~10–15× faster when
|
| 1116 |
-
processing large batches (e.g. 50 000 matrices at a time).
|
| 1117 |
-
"""
|
| 1118 |
-
p1 = batch[:, :, :, 0] # (N, R, C)
|
| 1119 |
-
p2 = batch[:, :, :, 1] # (N, R, C)
|
| 1120 |
-
|
| 1121 |
-
# Best P1 can achieve in each column (broadcast over rows dimension)
|
| 1122 |
-
p1_col_max = p1.max(axis=1, keepdims=True) # (N, 1, C)
|
| 1123 |
-
# Best P2 can achieve in each row (broadcast over cols dimension)
|
| 1124 |
-
p2_row_max = p2.max(axis=2, keepdims=True) # (N, R, 1)
|
| 1125 |
-
|
| 1126 |
-
# A cell is NE iff both players are already at their column/row maximum
|
| 1127 |
-
is_ne = (p1 == p1_col_max) & (p2 == p2_row_max) # (N, R, C)
|
| 1128 |
-
ne_counts = is_ne.sum(axis=(1, 2)).astype(np.int32) # (N,)
|
| 1129 |
-
|
| 1130 |
-
return is_ne, ne_counts
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
# ---------------------------------------------------------------------------
|
| 1134 |
-
# CSV I/O
|
| 1135 |
-
# ---------------------------------------------------------------------------
|
| 1136 |
-
|
| 1137 |
-
def _build_headers(rows: int, cols: int) -> List[str]:
|
| 1138 |
-
"""Return column headers for a rows×cols payoff matrix CSV."""
|
| 1139 |
-
headers = []
|
| 1140 |
-
for r in range(rows):
|
| 1141 |
-
for c in range(cols):
|
| 1142 |
-
headers += [f"r{r}c{c}_p1", f"r{r}c{c}_p2"]
|
| 1143 |
-
headers += ["num_equilibria", "equilibrium_positions", "category"]
|
| 1144 |
-
return headers
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
def save_dual_matrices_to_csv(matrices: List[np.ndarray], filename: str) -> None:
|
| 1148 |
-
"""
|
| 1149 |
-
Analyse each matrix for Nash equilibria and write results to a CSV file.
|
| 1150 |
-
|
| 1151 |
-
CSV columns (2×2 example):
|
| 1152 |
-
r0c0_p1, r0c0_p2, r0c1_p1, r0c1_p2,
|
| 1153 |
-
r1c0_p1, r1c0_p2, r1c1_p1, r1c1_p2,
|
| 1154 |
-
num_equilibria, equilibrium_positions, category
|
| 1155 |
-
|
| 1156 |
-
Args:
|
| 1157 |
-
matrices: list of numpy arrays with shape (rows, cols, 2)
|
| 1158 |
-
filename: output CSV path
|
| 1159 |
-
"""
|
| 1160 |
-
if not matrices:
|
| 1161 |
-
return
|
| 1162 |
-
|
| 1163 |
-
rows, cols = matrices[0].shape[0], matrices[0].shape[1]
|
| 1164 |
-
headers = _build_headers(rows, cols)
|
| 1165 |
-
|
| 1166 |
-
with open(filename, "w", newline="") as f:
|
| 1167 |
-
writer = csv.writer(f)
|
| 1168 |
-
writer.writerow(headers)
|
| 1169 |
-
for matrix in matrices:
|
| 1170 |
-
_write_matrix_row(writer, matrix)
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
def save_dual_matrices_iter_to_csv(
|
| 1174 |
-
matrix_iter: Iterator[np.ndarray],
|
| 1175 |
-
filename: str,
|
| 1176 |
-
rows: int,
|
| 1177 |
-
cols: int,
|
| 1178 |
-
progress_interval: int = 1_000_000,
|
| 1179 |
-
) -> int:
|
| 1180 |
-
"""
|
| 1181 |
-
Stream matrices from an iterator directly into a CSV — constant RAM usage
|
| 1182 |
-
regardless of dataset size. Prints progress every progress_interval rows.
|
| 1183 |
-
|
| 1184 |
-
Returns the total number of matrices written.
|
| 1185 |
-
"""
|
| 1186 |
-
headers = _build_headers(rows, cols)
|
| 1187 |
-
count = 0
|
| 1188 |
-
|
| 1189 |
-
with open(filename, "w", newline="") as f:
|
| 1190 |
-
writer = csv.writer(f)
|
| 1191 |
-
writer.writerow(headers)
|
| 1192 |
-
for matrix in matrix_iter:
|
| 1193 |
-
_write_matrix_row(writer, matrix)
|
| 1194 |
-
count += 1
|
| 1195 |
-
if count % progress_interval == 0:
|
| 1196 |
-
print(f" {count:,} matrices written…")
|
| 1197 |
-
|
| 1198 |
-
return count
|
| 1199 |
-
|
| 1200 |
-
|
| 1201 |
-
def save_batched_to_csv(
|
| 1202 |
-
rows: int,
|
| 1203 |
-
cols: int,
|
| 1204 |
-
min_val: int,
|
| 1205 |
-
max_val: int,
|
| 1206 |
-
filename: str,
|
| 1207 |
-
batch_size: int = 50_000,
|
| 1208 |
-
progress_interval: int = 1_000_000,
|
| 1209 |
-
) -> int:
|
| 1210 |
-
"""
|
| 1211 |
-
Generate all matrices for (rows, cols, min_val, max_val) and write to CSV
|
| 1212 |
-
using batched vectorised NE detection. Streams to disk — constant RAM.
|
| 1213 |
-
|
| 1214 |
-
Significantly faster than save_dual_matrices_iter_to_csv() for large ranges.
|
| 1215 |
-
Returns total number of matrices written.
|
| 1216 |
-
"""
|
| 1217 |
-
headers = _build_headers(rows, cols)
|
| 1218 |
-
count = 0
|
| 1219 |
-
|
| 1220 |
-
with open(filename, "w", newline="") as f:
|
| 1221 |
-
writer = csv.writer(f)
|
| 1222 |
-
writer.writerow(headers)
|
| 1223 |
-
|
| 1224 |
-
for batch in generate_dual_matrices_batched(rows, cols, min_val, max_val, batch_size):
|
| 1225 |
-
is_ne, ne_counts = find_nash_batch_vectorized(batch)
|
| 1226 |
-
B = len(batch)
|
| 1227 |
-
|
| 1228 |
-
for i in range(B):
|
| 1229 |
-
flat = batch[i].reshape(-1).tolist()
|
| 1230 |
-
n_eq = int(ne_counts[i])
|
| 1231 |
-
positions = [
|
| 1232 |
-
(r, c)
|
| 1233 |
-
for r in range(rows)
|
| 1234 |
-
for c in range(cols)
|
| 1235 |
-
if is_ne[i, r, c]
|
| 1236 |
-
]
|
| 1237 |
-
category = "Solved" if n_eq > 0 else "Unsolved"
|
| 1238 |
-
writer.writerow(
|
| 1239 |
-
flat + [n_eq, str(positions) if positions else "None", category]
|
| 1240 |
-
)
|
| 1241 |
-
count += 1
|
| 1242 |
-
if count % progress_interval == 0:
|
| 1243 |
-
print(f" {count:,} matrices written…")
|
| 1244 |
-
|
| 1245 |
-
return count
|
| 1246 |
-
|
| 1247 |
-
|
| 1248 |
-
def _write_matrix_row(writer: csv.writer, matrix: np.ndarray) -> None:
|
| 1249 |
-
"""Write one matrix as a CSV row (helper used by both save functions)."""
|
| 1250 |
-
flat_values = matrix.reshape(-1).tolist()
|
| 1251 |
-
equilibria = find_nash_equilibria(matrix)
|
| 1252 |
-
category = "Solved" if equilibria else "Unsolved"
|
| 1253 |
-
writer.writerow(
|
| 1254 |
-
flat_values
|
| 1255 |
-
+ [len(equilibria), str(equilibria) if equilibria else "None", category]
|
| 1256 |
-
)
|
| 1257 |
-
|
| 1258 |
-
|
| 1259 |
-
# ---------------------------------------------------------------------------
|
| 1260 |
-
# Display utilities
|
| 1261 |
-
# ---------------------------------------------------------------------------
|
| 1262 |
-
|
| 1263 |
-
def print_dual_matrices(matrices: List[np.ndarray]) -> None:
|
| 1264 |
-
"""Print matrices in a human-readable format."""
|
| 1265 |
-
for i, matrix in enumerate(matrices):
|
| 1266 |
-
print(f"Matrix {i + 1}:")
|
| 1267 |
-
for row in matrix:
|
| 1268 |
-
print([f"({pair[0]},{pair[1]})" for pair in row])
|
| 1269 |
-
print()
|
| 1270 |
-
|
| 1271 |
-
|
| 1272 |
-
# ---------------------------------------------------------------------------
|
| 1273 |
-
# Backwards-compatibility aliases
|
| 1274 |
-
# ---------------------------------------------------------------------------
|
| 1275 |
-
|
| 1276 |
-
def generate_2x2_dual_matrices(min_val: int = 0, max_val: int = 5) -> List[np.ndarray]:
|
| 1277 |
-
"""Alias for generate_dual_matrices(2, 2, ...) — kept for compatibility."""
|
| 1278 |
-
return generate_dual_matrices(2, 2, min_val, max_val)
|
| 1279 |
-
|
| 1280 |
-
</gradio-file><gradio-file name="src/theme.py">
|
| 1281 |
-
"""
|
| 1282 |
-
theme.py — UI theme tokens for the public Hugging Face Space.
|
| 1283 |
-
"""
|
| 1284 |
-
|
| 1285 |
-
BG_PAGE = "#0F0F0F"
|
| 1286 |
-
BG_SURFACE = "#1A1410"
|
| 1287 |
-
BG_ALT = "#15110D"
|
| 1288 |
-
|
| 1289 |
-
BORDER_STRONG = "#3D2418"
|
| 1290 |
-
BORDER_SOFT = "#2A2520"
|
| 1291 |
-
|
| 1292 |
-
ACCENT = "#E8610A"
|
| 1293 |
-
ACCENT_DIM = "#8C3A06"
|
| 1294 |
-
|
| 1295 |
-
TEXT_PRI = "#F0EDE8"
|
| 1296 |
-
TEXT_MUT = "#7A7570"
|
| 1297 |
-
TEXT_DIM = "#4A4540"
|
| 1298 |
-
|
| 1299 |
-
|
| 1300 |
-
GRADIO_CSS = """
|
| 1301 |
-
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
| 1302 |
-
|
| 1303 |
-
:root {
|
| 1304 |
-
--bg-page: #0F0F0F;
|
| 1305 |
-
--bg-surface: #1A1410;
|
| 1306 |
-
--bg-alt: #15110D;
|
| 1307 |
-
--border-strong: #3D2418;
|
| 1308 |
-
--border-soft: #2A2520;
|
| 1309 |
-
--accent: #E8610A;
|
| 1310 |
-
--accent-dim: #8C3A06;
|
| 1311 |
-
--text-pri: #F0EDE8;
|
| 1312 |
-
--text-mut: #7A7570;
|
| 1313 |
-
--text-dim: #4A4540;
|
| 1314 |
-
--radius: 6px;
|
| 1315 |
-
}
|
| 1316 |
-
|
| 1317 |
-
body, .gradio-container {
|
| 1318 |
-
background-color: var(--bg-page) !important;
|
| 1319 |
-
color: var(--text-pri) !important;
|
| 1320 |
-
font-family: 'IBM Plex Mono', 'Courier New', monospace !important;
|
| 1321 |
-
}
|
| 1322 |
-
|
| 1323 |
-
h1, h2, h3, h4, .section-label {
|
| 1324 |
-
font-family: 'Space Grotesk', sans-serif !important;
|
| 1325 |
-
font-weight: 700 !important;
|
| 1326 |
-
text-transform: uppercase !important;
|
| 1327 |
-
letter-spacing: -0.02em !important;
|
| 1328 |
-
color: var(--text-pri) !important;
|
| 1329 |
-
}
|
| 1330 |
-
|
| 1331 |
-
.section-num {
|
| 1332 |
-
color: var(--accent) !important;
|
| 1333 |
-
font-family: 'Space Grotesk', sans-serif !important;
|
| 1334 |
-
font-weight: 700 !important;
|
| 1335 |
-
font-size: 0.75rem !important;
|
| 1336 |
-
text-transform: uppercase !important;
|
| 1337 |
-
letter-spacing: 0.12em !important;
|
| 1338 |
-
}
|
| 1339 |
-
|
| 1340 |
-
.block, .panel, .gr-box, .gr-form {
|
| 1341 |
-
background-color: var(--bg-surface) !important;
|
| 1342 |
-
border: 1px solid var(--border-strong) !important;
|
| 1343 |
-
border-radius: var(--radius) !important;
|
| 1344 |
-
}
|
| 1345 |
-
|
| 1346 |
-
.tabs > .tab-nav {
|
| 1347 |
-
background-color: var(--bg-alt) !important;
|
| 1348 |
-
border-bottom: 1px solid var(--border-strong) !important;
|
| 1349 |
-
}
|
| 1350 |
-
|
| 1351 |
-
.tabs > .tab-nav > button {
|
| 1352 |
-
color: var(--text-mut) !important;
|
| 1353 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1354 |
-
font-size: 0.78rem !important;
|
| 1355 |
-
text-transform: uppercase !important;
|
| 1356 |
-
letter-spacing: 0.08em !important;
|
| 1357 |
-
border-bottom: 2px solid transparent !important;
|
| 1358 |
-
padding: 0.55rem 1.1rem !important;
|
| 1359 |
-
background: transparent !important;
|
| 1360 |
-
transition: color 0.15s, border-color 0.15s;
|
| 1361 |
-
}
|
| 1362 |
-
|
| 1363 |
-
.tabs > .tab-nav > button.selected,
|
| 1364 |
-
.tabs > .tab-nav > button:hover {
|
| 1365 |
-
color: var(--accent) !important;
|
| 1366 |
-
border-bottom-color: var(--accent) !important;
|
| 1367 |
-
}
|
| 1368 |
-
|
| 1369 |
-
input, textarea, select, .gr-input, .gr-textbox {
|
| 1370 |
-
background-color: var(--bg-alt) !important;
|
| 1371 |
-
border: 1px solid var(--border-strong) !important;
|
| 1372 |
-
color: var(--text-pri) !important;
|
| 1373 |
-
border-radius: var(--radius) !important;
|
| 1374 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1375 |
-
}
|
| 1376 |
-
|
| 1377 |
-
input:focus, textarea:focus {
|
| 1378 |
-
border-color: var(--accent) !important;
|
| 1379 |
-
outline: none !important;
|
| 1380 |
-
box-shadow: 0 0 0 2px rgba(232, 97, 10, 0.25) !important;
|
| 1381 |
-
}
|
| 1382 |
-
|
| 1383 |
-
button.primary, .gr-button-primary {
|
| 1384 |
-
background-color: var(--accent) !important;
|
| 1385 |
-
color: #fff !important;
|
| 1386 |
-
border: none !important;
|
| 1387 |
-
border-radius: var(--radius) !important;
|
| 1388 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1389 |
-
font-weight: 500 !important;
|
| 1390 |
-
letter-spacing: 0.04em !important;
|
| 1391 |
-
transition: background-color 0.15s;
|
| 1392 |
-
}
|
| 1393 |
-
|
| 1394 |
-
button.primary:hover {
|
| 1395 |
-
background-color: var(--accent-dim) !important;
|
| 1396 |
-
}
|
| 1397 |
-
|
| 1398 |
-
button.secondary, .gr-button-secondary {
|
| 1399 |
-
background-color: transparent !important;
|
| 1400 |
-
color: var(--text-mut) !important;
|
| 1401 |
-
border: 1px solid var(--border-strong) !important;
|
| 1402 |
-
border-radius: var(--radius) !important;
|
| 1403 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1404 |
-
}
|
| 1405 |
-
|
| 1406 |
-
button.secondary:hover {
|
| 1407 |
-
border-color: var(--accent) !important;
|
| 1408 |
-
color: var(--accent) !important;
|
| 1409 |
-
}
|
| 1410 |
-
|
| 1411 |
-
table, .gr-dataframe table {
|
| 1412 |
-
background-color: var(--bg-surface) !important;
|
| 1413 |
-
color: var(--text-pri) !important;
|
| 1414 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1415 |
-
font-size: 0.82rem !important;
|
| 1416 |
-
border-collapse: collapse !important;
|
| 1417 |
-
}
|
| 1418 |
-
|
| 1419 |
-
th {
|
| 1420 |
-
background-color: var(--bg-alt) !important;
|
| 1421 |
-
color: var(--accent) !important;
|
| 1422 |
-
font-weight: 500 !important;
|
| 1423 |
-
text-transform: uppercase !important;
|
| 1424 |
-
letter-spacing: 0.06em !important;
|
| 1425 |
-
padding: 6px 10px !important;
|
| 1426 |
-
border-bottom: 1px solid var(--border-strong) !important;
|
| 1427 |
-
}
|
| 1428 |
-
|
| 1429 |
-
td {
|
| 1430 |
-
padding: 4px 10px !important;
|
| 1431 |
-
border-bottom: 1px solid var(--border-soft) !important;
|
| 1432 |
-
}
|
| 1433 |
-
|
| 1434 |
-
tr:nth-child(even) td {
|
| 1435 |
-
background-color: var(--bg-alt) !important;
|
| 1436 |
-
}
|
| 1437 |
-
|
| 1438 |
-
tr:hover td {
|
| 1439 |
-
background-color: rgba(232, 97, 10, 0.06) !important;
|
| 1440 |
-
}
|
| 1441 |
-
|
| 1442 |
-
.gr-dropdown, .gr-select {
|
| 1443 |
-
background-color: var(--bg-alt) !important;
|
| 1444 |
-
border: 1px solid var(--border-strong) !important;
|
| 1445 |
-
color: var(--text-pri) !important;
|
| 1446 |
-
border-radius: var(--radius) !important;
|
| 1447 |
-
}
|
| 1448 |
-
|
| 1449 |
-
.prose, .gr-markdown {
|
| 1450 |
-
color: var(--text-pri) !important;
|
| 1451 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1452 |
-
line-height: 1.65 !important;
|
| 1453 |
-
}
|
| 1454 |
-
|
| 1455 |
-
.prose strong, .gr-markdown strong {
|
| 1456 |
-
color: var(--accent) !important;
|
| 1457 |
-
}
|
| 1458 |
-
|
| 1459 |
-
.prose code, .gr-markdown code {
|
| 1460 |
-
background-color: var(--bg-alt) !important;
|
| 1461 |
-
color: var(--text-pri) !important;
|
| 1462 |
-
padding: 0.1rem 0.3rem !important;
|
| 1463 |
-
border-radius: 4px !important;
|
| 1464 |
-
border: 1px solid var(--border-soft) !important;
|
| 1465 |
-
}
|
| 1466 |
-
|
| 1467 |
-
label {
|
| 1468 |
-
color: var(--text-mut) !important;
|
| 1469 |
-
font-family: 'IBM Plex Mono', monospace !important;
|
| 1470 |
-
font-size: 0.78rem !important;
|
| 1471 |
-
letter-spacing: 0.04em !important;
|
| 1472 |
-
}
|
| 1473 |
-
"""
|
| 1474 |
-
|
| 1475 |
-
</gradio-file>
|
| 1476 |
-
</gradio-lite>
|
| 1477 |
-
<script type="module" src="https://cdn.jsdelivr.net/npm/@gradio/lite/dist/lite.js"></script>
|
| 1478 |
</body>
|
| 1479 |
</html>
|
|
|
|
| 4 |
<meta charset="utf-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
<title>Game Theory Matrix Classifier</title>
|
| 7 |
+
<meta
|
| 8 |
+
name="description"
|
| 9 |
+
content="Static 2x2 game theory matrix explorer with Nash equilibrium detection and classic game classification."
|
| 10 |
+
>
|
| 11 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 12 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 13 |
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
|
| 14 |
+
<link rel="stylesheet" href="./styles.css">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
</head>
|
| 16 |
<body>
|
| 17 |
+
<main class="page-shell">
|
| 18 |
+
<section class="hero card">
|
| 19 |
+
<p class="eyebrow">Public Space / Static / 2x2 only</p>
|
| 20 |
+
<h1>Game Theory Matrix Classifier</h1>
|
| 21 |
+
<p class="hero-copy">
|
| 22 |
+
Lightweight public demo. Enter any 2x2 payoff matrix, find pure-strategy Nash equilibria,
|
| 23 |
+
compute mixed equilibrium when needed, classify classic game type.
|
| 24 |
+
</p>
|
| 25 |
+
</section>
|
| 26 |
+
|
| 27 |
+
<section class="layout">
|
| 28 |
+
<aside class="card controls">
|
| 29 |
+
<div class="section-head">
|
| 30 |
+
<p class="eyebrow">Inputs</p>
|
| 31 |
+
<h2>Matrix Builder</h2>
|
| 32 |
+
</div>
|
| 33 |
+
|
| 34 |
+
<label class="control-label" for="preset">Load preset</label>
|
| 35 |
+
<select id="preset" class="control-input"></select>
|
| 36 |
+
|
| 37 |
+
<div class="grid-head">
|
| 38 |
+
<span>Top row</span>
|
| 39 |
+
<span>Bottom row</span>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div class="input-grid">
|
| 43 |
+
<label class="payoff-card">
|
| 44 |
+
<span class="payoff-label">(0,0)</span>
|
| 45 |
+
<div class="payoff-pair">
|
| 46 |
+
<input id="r0c0_p1" class="control-input" type="number" step="1" value="3">
|
| 47 |
+
<input id="r0c0_p2" class="control-input" type="number" step="1" value="3">
|
| 48 |
+
</div>
|
| 49 |
+
</label>
|
| 50 |
+
|
| 51 |
+
<label class="payoff-card">
|
| 52 |
+
<span class="payoff-label">(0,1)</span>
|
| 53 |
+
<div class="payoff-pair">
|
| 54 |
+
<input id="r0c1_p1" class="control-input" type="number" step="1" value="0">
|
| 55 |
+
<input id="r0c1_p2" class="control-input" type="number" step="1" value="5">
|
| 56 |
+
</div>
|
| 57 |
+
</label>
|
| 58 |
+
|
| 59 |
+
<label class="payoff-card">
|
| 60 |
+
<span class="payoff-label">(1,0)</span>
|
| 61 |
+
<div class="payoff-pair">
|
| 62 |
+
<input id="r1c0_p1" class="control-input" type="number" step="1" value="5">
|
| 63 |
+
<input id="r1c0_p2" class="control-input" type="number" step="1" value="0">
|
| 64 |
+
</div>
|
| 65 |
+
</label>
|
| 66 |
+
|
| 67 |
+
<label class="payoff-card">
|
| 68 |
+
<span class="payoff-label">(1,1)</span>
|
| 69 |
+
<div class="payoff-pair">
|
| 70 |
+
<input id="r1c1_p1" class="control-input" type="number" step="1" value="1">
|
| 71 |
+
<input id="r1c1_p2" class="control-input" type="number" step="1" value="1">
|
| 72 |
+
</div>
|
| 73 |
+
</label>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
<div class="button-row">
|
| 77 |
+
<button id="analyze" class="primary-button" type="button">Analyze Matrix</button>
|
| 78 |
+
<button id="reset" class="ghost-button" type="button">Reset</button>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div class="mini-note">
|
| 82 |
+
Presets include Prisoner's Dilemma, Battle of the Sexes, Stag Hunt, Chicken, Coordination, Matching Pennies.
|
| 83 |
+
</div>
|
| 84 |
+
</aside>
|
| 85 |
+
|
| 86 |
+
<section class="results">
|
| 87 |
+
<div class="card">
|
| 88 |
+
<div class="section-head">
|
| 89 |
+
<p class="eyebrow">Output 01</p>
|
| 90 |
+
<h2>Equilibrium Explorer</h2>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
+
<div class="matrix-wrap">
|
| 94 |
+
<table class="matrix-table">
|
| 95 |
+
<thead>
|
| 96 |
+
<tr>
|
| 97 |
+
<th>Col 0</th>
|
| 98 |
+
<th>Col 1</th>
|
| 99 |
+
</tr>
|
| 100 |
+
</thead>
|
| 101 |
+
<tbody id="matrix-body"></tbody>
|
| 102 |
+
</table>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<div id="summary" class="summary-card"></div>
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<div class="card">
|
| 109 |
+
<div class="section-head">
|
| 110 |
+
<p class="eyebrow">Output 02</p>
|
| 111 |
+
<h2>Classification</h2>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div id="game-type-badge" class="game-type-badge"></div>
|
| 115 |
+
<p id="game-type-description" class="game-type-description"></p>
|
| 116 |
+
|
| 117 |
+
<div class="table-wrap">
|
| 118 |
+
<table class="properties-table">
|
| 119 |
+
<thead>
|
| 120 |
+
<tr>
|
| 121 |
+
<th>Property</th>
|
| 122 |
+
<th>Value</th>
|
| 123 |
+
<th>Note</th>
|
| 124 |
+
</tr>
|
| 125 |
+
</thead>
|
| 126 |
+
<tbody id="properties-body"></tbody>
|
| 127 |
+
</table>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
</section>
|
| 131 |
+
</section>
|
| 132 |
+
</main>
|
| 133 |
+
|
| 134 |
+
<script type="module" src="./app.mjs"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
</body>
|
| 136 |
</html>
|
style.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
-
|
| 6 |
-
h1 {
|
| 7 |
-
font-size: 16px;
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
styles.css
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
--bg-page: #0f0f0f;
|
| 3 |
+
--bg-surface: #1a1410;
|
| 4 |
+
--bg-alt: #15110d;
|
| 5 |
+
--border-strong: #3d2418;
|
| 6 |
+
--border-soft: #2a2520;
|
| 7 |
+
--accent: #e8610a;
|
| 8 |
+
--accent-dim: #8c3a06;
|
| 9 |
+
--text-pri: #f0ede8;
|
| 10 |
+
--text-mut: #9a9288;
|
| 11 |
+
--text-dim: #62584f;
|
| 12 |
+
--good: #4caf82;
|
| 13 |
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.32);
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
* {
|
| 17 |
+
box-sizing: border-box;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
html,
|
| 21 |
+
body {
|
| 22 |
+
margin: 0;
|
| 23 |
+
min-height: 100%;
|
| 24 |
+
background:
|
| 25 |
+
radial-gradient(circle at top right, rgba(232, 97, 10, 0.16), transparent 26rem),
|
| 26 |
+
radial-gradient(circle at bottom left, rgba(58, 155, 213, 0.08), transparent 24rem),
|
| 27 |
+
linear-gradient(180deg, #0f0f0f 0%, #15100c 100%);
|
| 28 |
+
color: var(--text-pri);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
body {
|
| 32 |
+
font-family: "IBM Plex Mono", monospace;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
button,
|
| 36 |
+
input,
|
| 37 |
+
select {
|
| 38 |
+
font: inherit;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.page-shell {
|
| 42 |
+
width: min(1200px, calc(100% - 2rem));
|
| 43 |
+
margin: 0 auto;
|
| 44 |
+
padding: 1rem 0 2rem;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.layout {
|
| 48 |
+
display: grid;
|
| 49 |
+
gap: 1rem;
|
| 50 |
+
grid-template-columns: minmax(320px, 360px) minmax(0, 1fr);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.results {
|
| 54 |
+
display: grid;
|
| 55 |
+
gap: 1rem;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.card {
|
| 59 |
+
background: linear-gradient(180deg, rgba(26, 20, 16, 0.98), rgba(21, 17, 13, 0.98));
|
| 60 |
+
border: 1px solid var(--border-strong);
|
| 61 |
+
border-radius: 18px;
|
| 62 |
+
box-shadow: var(--shadow);
|
| 63 |
+
padding: 1.15rem;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.hero {
|
| 67 |
+
margin-bottom: 1rem;
|
| 68 |
+
padding: 1.4rem 1.2rem 1.3rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.hero h1,
|
| 72 |
+
.section-head h2 {
|
| 73 |
+
font-family: "Space Grotesk", sans-serif;
|
| 74 |
+
font-weight: 700;
|
| 75 |
+
letter-spacing: -0.03em;
|
| 76 |
+
margin: 0;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.hero h1 {
|
| 80 |
+
font-size: clamp(1.8rem, 4vw, 3rem);
|
| 81 |
+
text-transform: uppercase;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.hero-copy {
|
| 85 |
+
color: var(--text-mut);
|
| 86 |
+
line-height: 1.7;
|
| 87 |
+
max-width: 58rem;
|
| 88 |
+
margin: 0.9rem 0 0;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.eyebrow {
|
| 92 |
+
margin: 0 0 0.55rem;
|
| 93 |
+
color: var(--accent);
|
| 94 |
+
font-size: 0.74rem;
|
| 95 |
+
letter-spacing: 0.14em;
|
| 96 |
+
text-transform: uppercase;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.section-head {
|
| 100 |
+
margin-bottom: 1rem;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.controls {
|
| 104 |
+
height: fit-content;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.control-label,
|
| 108 |
+
.grid-head,
|
| 109 |
+
.payoff-label,
|
| 110 |
+
.mini-note {
|
| 111 |
+
color: var(--text-mut);
|
| 112 |
+
font-size: 0.78rem;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.control-input {
|
| 116 |
+
width: 100%;
|
| 117 |
+
background: var(--bg-alt);
|
| 118 |
+
color: var(--text-pri);
|
| 119 |
+
border: 1px solid var(--border-strong);
|
| 120 |
+
border-radius: 12px;
|
| 121 |
+
padding: 0.82rem 0.9rem;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.control-input:focus {
|
| 125 |
+
outline: none;
|
| 126 |
+
border-color: var(--accent);
|
| 127 |
+
box-shadow: 0 0 0 3px rgba(232, 97, 10, 0.14);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.grid-head {
|
| 131 |
+
display: flex;
|
| 132 |
+
justify-content: space-between;
|
| 133 |
+
text-transform: uppercase;
|
| 134 |
+
letter-spacing: 0.1em;
|
| 135 |
+
margin: 1rem 0 0.7rem;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.input-grid {
|
| 139 |
+
display: grid;
|
| 140 |
+
gap: 0.75rem;
|
| 141 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.payoff-card {
|
| 145 |
+
display: grid;
|
| 146 |
+
gap: 0.55rem;
|
| 147 |
+
padding: 0.9rem;
|
| 148 |
+
background: rgba(255, 255, 255, 0.015);
|
| 149 |
+
border: 1px solid var(--border-soft);
|
| 150 |
+
border-radius: 14px;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.payoff-pair {
|
| 154 |
+
display: grid;
|
| 155 |
+
gap: 0.5rem;
|
| 156 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.button-row {
|
| 160 |
+
display: flex;
|
| 161 |
+
gap: 0.7rem;
|
| 162 |
+
margin-top: 1rem;
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
.primary-button,
|
| 166 |
+
.ghost-button {
|
| 167 |
+
border-radius: 12px;
|
| 168 |
+
padding: 0.9rem 1rem;
|
| 169 |
+
cursor: pointer;
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
.primary-button {
|
| 173 |
+
flex: 1 1 auto;
|
| 174 |
+
background: var(--accent);
|
| 175 |
+
color: white;
|
| 176 |
+
border: none;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.primary-button:hover {
|
| 180 |
+
background: var(--accent-dim);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.ghost-button {
|
| 184 |
+
background: transparent;
|
| 185 |
+
color: var(--text-mut);
|
| 186 |
+
border: 1px solid var(--border-strong);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.ghost-button:hover {
|
| 190 |
+
border-color: var(--accent);
|
| 191 |
+
color: var(--accent);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.mini-note {
|
| 195 |
+
margin-top: 0.85rem;
|
| 196 |
+
line-height: 1.6;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.matrix-wrap {
|
| 200 |
+
overflow-x: auto;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.matrix-table,
|
| 204 |
+
.properties-table {
|
| 205 |
+
width: 100%;
|
| 206 |
+
border-collapse: collapse;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.matrix-table th,
|
| 210 |
+
.properties-table th {
|
| 211 |
+
color: var(--accent);
|
| 212 |
+
background: rgba(255, 255, 255, 0.015);
|
| 213 |
+
border-bottom: 1px solid var(--border-strong);
|
| 214 |
+
padding: 0.85rem;
|
| 215 |
+
text-align: left;
|
| 216 |
+
text-transform: uppercase;
|
| 217 |
+
letter-spacing: 0.08em;
|
| 218 |
+
font-size: 0.72rem;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.matrix-cell,
|
| 222 |
+
.properties-table td {
|
| 223 |
+
border-bottom: 1px solid var(--border-soft);
|
| 224 |
+
padding: 0.9rem 0.85rem;
|
| 225 |
+
vertical-align: top;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.matrix-cell {
|
| 229 |
+
width: 50%;
|
| 230 |
+
background: rgba(255, 255, 255, 0.015);
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
.matrix-cell.is-ne {
|
| 234 |
+
background: rgba(232, 97, 10, 0.1);
|
| 235 |
+
box-shadow: inset 0 0 0 1px rgba(232, 97, 10, 0.38);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.cell-coord,
|
| 239 |
+
.cell-tag {
|
| 240 |
+
color: var(--text-dim);
|
| 241 |
+
font-size: 0.72rem;
|
| 242 |
+
text-transform: uppercase;
|
| 243 |
+
letter-spacing: 0.08em;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.cell-payoff {
|
| 247 |
+
font-family: "Space Grotesk", sans-serif;
|
| 248 |
+
font-size: 1.35rem;
|
| 249 |
+
margin: 0.3rem 0;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.cell-tag {
|
| 253 |
+
color: var(--accent);
|
| 254 |
+
min-height: 1rem;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.summary-card {
|
| 258 |
+
display: grid;
|
| 259 |
+
gap: 0.45rem;
|
| 260 |
+
margin-top: 1rem;
|
| 261 |
+
padding: 1rem;
|
| 262 |
+
border: 1px solid var(--border-strong);
|
| 263 |
+
border-radius: 14px;
|
| 264 |
+
background: rgba(255, 255, 255, 0.015);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.summary-line {
|
| 268 |
+
color: var(--text-mut);
|
| 269 |
+
line-height: 1.6;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.summary-line strong {
|
| 273 |
+
color: var(--text-pri);
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.game-type-badge {
|
| 277 |
+
display: inline-flex;
|
| 278 |
+
align-items: center;
|
| 279 |
+
width: fit-content;
|
| 280 |
+
border: 1px solid currentColor;
|
| 281 |
+
border-radius: 999px;
|
| 282 |
+
padding: 0.5rem 0.85rem;
|
| 283 |
+
font-family: "Space Grotesk", sans-serif;
|
| 284 |
+
font-size: 1.4rem;
|
| 285 |
+
font-weight: 700;
|
| 286 |
+
letter-spacing: -0.02em;
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.game-type-description {
|
| 290 |
+
color: var(--text-mut);
|
| 291 |
+
line-height: 1.75;
|
| 292 |
+
margin: 1rem 0 1.2rem;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.table-wrap {
|
| 296 |
+
overflow-x: auto;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.properties-table td:nth-child(2) {
|
| 300 |
+
color: var(--text-pri);
|
| 301 |
+
white-space: nowrap;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.properties-table td:nth-child(3) {
|
| 305 |
+
color: var(--text-dim);
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
.bool-yes {
|
| 309 |
+
color: var(--good);
|
| 310 |
+
font-weight: 700;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.bool-no {
|
| 314 |
+
color: var(--text-dim);
|
| 315 |
+
font-weight: 700;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
@media (max-width: 900px) {
|
| 319 |
+
.layout {
|
| 320 |
+
grid-template-columns: 1fr;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.page-shell {
|
| 324 |
+
width: min(100% - 1rem, 1200px);
|
| 325 |
+
}
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
@media (max-width: 560px) {
|
| 329 |
+
.input-grid {
|
| 330 |
+
grid-template-columns: 1fr;
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.button-row {
|
| 334 |
+
flex-direction: column;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.cell-payoff {
|
| 338 |
+
font-size: 1.1rem;
|
| 339 |
+
}
|
| 340 |
+
}
|