Below is a classic two-player game. Player A picks a row, Player B picks a column. The payoff matrices show each player's reward. Each player can also mix strategies — choosing Row 1 with probability p and Row/Column 2 with probability 1−p.
<p class="hint">
A 2×2 coordination game. Player A picks Row, Player B picks Column.
Each cell shows A's / B's payoff. Drag the sliders to set each player's
mixing probability and find the point where
<b>neither player can improve unilaterally</b> — that’s a Nash equilibrium.
</p>
<div class="game-wrap">
<table class="payoff-table">
<thead>
<tr>
<th></th>
<th>B: Col 1 (<span id="bpct">50</span>%)</th>
<th>B: Col 2 (<span id="bpct2">50</span>%)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="rlabel">A: Row 1 (<span id="apct">50</span>%)</td>
<td id="c00" class="cell">2 / 1</td>
<td id="c01" class="cell">0 / 0</td>
</tr>
<tr>
<td class="rlabel">A: Row 2 (<span id="apct2">50</span>%)</td>
<td id="c10" class="cell">0 / 0</td>
<td id="c11" class="cell">1 / 2</td>
</tr>
</tbody>
</table>
<div class="sliders">
<label>Player A plays Row 1 with probability <b id="pa-val">0.50</b>
<input type="range" id="pa" min="0" max="100" value="50">
</label>
<label>Player B plays Col 1 with probability <b id="pb-val">0.50</b>
<input type="range" id="pb" min="0" max="100" value="50">
</label>
</div>
<div class="payoffs">
<div class="pblock" id="pa-block">
<span class="plabel">A's expected payoff</span>
<span class="pnum" id="pa-payoff">–</span>
</div>
<div class="pblock" id="pb-block">
<span class="plabel">B's expected payoff</span>
<span class="pnum" id="pb-payoff">–</span>
</div>
</div>
<div class="status" id="status">Adjust the sliders to search for a Nash equilibrium.</div>
<button id="findNash" type="button">Find Nash Equilibrium</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.game-wrap { display: flex; flex-direction: column; gap: .7rem; }
.payoff-table { border-collapse: collapse; font-size: .85rem; }
.payoff-table th, .payoff-table td { border: 1px solid #cdd9e3; padding: .4rem .7rem; text-align: center; }
.payoff-table th { background: #e8eef3; font-weight: 600; }
.rlabel { background: #e8eef3; font-weight: 600; text-align: left; white-space: nowrap; font-size: .82rem; }
.cell { font: 700 14px ui-monospace, monospace; min-width: 70px; transition: background .25s; }
.cell.hi-a { background: #dbe9ff; }
.cell.hi-b { background: #fde8c0; }
.cell.hi-ab { background: #d4f0da; }
.sliders { display: flex; flex-direction: column; gap: .5rem; }
.sliders label { display: flex; flex-direction: column; gap: .2rem; font-size: .88rem; }
.sliders input[type=range] { width: 100%; accent-color: #1d3557; }
.payoffs { display: flex; gap: 1rem; flex-wrap: wrap; }
.pblock { display: flex; flex-direction: column; align-items: center; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .4rem .8rem; min-width: 140px; }
.plabel { font-size: .75rem; color: #555; }
.pnum { font: 700 1.3rem ui-monospace, monospace; color: #1d3557; }
.pblock.nash { background: #d4f0da; border-color: #4caf50; }
.pblock.nash .pnum { color: #0a7d33; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.4em; color: #444; line-height: 1.4; }
.status.ok { color: #0a7d33; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2x2 coordination game — two pure Nash equilibria + one mixed Nash equilibrium.
// A: [[2,0],[0,1]] B: [[1,0],[0,2]]
// Pure Nash: (Row1,Col1) and (Row2,Col2).
// Mixed Nash: p* = 2/3, q* = 1/3 (found by making each player indifferent).
//
// B's Col-1 vs Col-2 payoff at row-mix p:
// Col1: B[0][0]*p + B[1][0]*(1-p) = 1*p + 0 = p
// Col2: B[0][1]*p + B[1][1]*(1-p) = 0 + 2*(1-p) = 2-2p
// Indiff: p = 2-2p => 3p=2 => p*=2/3
//
// A's Row-1 vs Row-2 payoff at col-mix q:
// Row1: A[0][0]*q + A[0][1]*(1-q) = 2q
// Row2: A[1][0]*q + A[1][1]*(1-q) = 0 + 1-q = 1-q
// Indiff: 2q = 1-q => 3q=1 => q*=1/3
const MA = [[2,0],[0,1]];
const MB = [[1,0],[0,2]];
// Compute Nash: p* such that B is indifferent, q* such that A is indifferent.
const dB_col = MB[0][0] - MB[1][0] - MB[0][1] + MB[1][1]; // 1-0-0+2=3
const pStar = dB_col !== 0 ? (MB[1][1] - MB[1][0]) / dB_col : 0.5; // (2-0)/3 = 2/3
const dA_row = MA[0][0] - MA[0][1] - MA[1][0] + MA[1][1]; // 2-0-0+1=3
const qStar = dA_row !== 0 ? (MA[1][1] - MA[0][1]) / dA_row : 0.5; // (1-0)/3 = 1/3
// Inline verification (runs at page load — open browser console to see)
console.assert(Math.abs(pStar - 2/3) < 1e-9, fmt('pStar should be 2/3, got %pStar%', { pStar: pStar }));
console.assert(Math.abs(qStar - 1/3) < 1e-9, fmt('qStar should be 1/3, got %qStar%', { qStar: qStar }));
function payoffA(p, q) {
return MA[0][0]*p*q + MA[0][1]*p*(1-q) + MA[1][0]*(1-p)*q + MA[1][1]*(1-p)*(1-q);
}
function payoffB(p, q) {
return MB[0][0]*p*q + MB[0][1]*p*(1-q) + MB[1][0]*(1-p)*q + MB[1][1]*(1-p)*(1-q);
}
function isNash(p, q, eps) {
const aAtP = payoffA(p, q);
const bAtQ = payoffB(p, q);
return aAtP >= payoffA(0, q) - eps && aAtP >= payoffA(1, q) - eps &&
bAtQ >= payoffB(p, 0) - eps && bAtQ >= payoffB(p, 1) - eps;
}
// Full Nash check (both assertions)
console.assert(isNash(pStar, qStar, 1e-9), 'Mixed Nash should satisfy isNash');
console.assert(isNash(1, 1, 1e-9), 'Pure Nash (Row1,Col1) should hold');
console.assert(isNash(0, 0, 1e-9), 'Pure Nash (Row2,Col2) should hold');
function render() {
const p = document.getElementById('pa').value / 100;
const q = document.getElementById('pb').value / 100;
document.getElementById('pa-val').textContent = p.toFixed(2);
document.getElementById('pb-val').textContent = q.toFixed(2);
document.getElementById('apct').textContent = Math.round(p * 100);
document.getElementById('apct2').textContent = Math.round((1-p) * 100);
document.getElementById('bpct').textContent = Math.round(q * 100);
document.getElementById('bpct2').textContent = Math.round((1-q) * 100);
const eA = payoffA(p, q);
const eB = payoffB(p, q);
document.getElementById('pa-payoff').textContent = eA.toFixed(3);
document.getElementById('pb-payoff').textContent = eB.toFixed(3);
// Highlight best-response cells
const cells = [['c00',0,0],['c01',0,1],['c10',1,0],['c11',1,1]];
const bestArow = payoffA(1,q) >= payoffA(0,q) ? 0 : 1;
const bestBcol = payoffB(p,1) >= payoffB(p,0) ? 1 : 0;
cells.forEach(([id, r, c]) => {
const el = document.getElementById(id);
el.className = 'cell';
const brA = r === bestArow;
const brB = c === bestBcol;
if (brA && brB) el.classList.add('hi-ab');
else if (brA) el.classList.add('hi-a');
else if (brB) el.classList.add('hi-b');
});
const nash = isNash(p, q, 0.06);
document.getElementById('pa-block').classList.toggle('nash', nash);
document.getElementById('pb-block').classList.toggle('nash', nash);
const status = document.getElementById('status');
if (nash) {
status.textContent = '✓ Nash equilibrium! Neither player benefits from changing their mix alone.';
status.className = 'status ok';
} else {
const aGain = Math.max(payoffA(0,q), payoffA(1,q)) - eA;
const bGain = Math.max(payoffB(p,0), payoffB(p,1)) - eB;
const who = (aGain > bGain + 0.001) ? 'Player A' : (bGain > aGain + 0.001 ? 'Player B' : 'Both players');
status.textContent = fmt('Not a Nash equilibrium — %who% could gain by changing strategy.', { who: who });
status.className = 'status';
}
}
document.getElementById('pa').addEventListener('input', render);
document.getElementById('pb').addEventListener('input', render);
document.getElementById('findNash').addEventListener('click', () => {
document.getElementById('pa').value = Math.round(pStar * 100);
document.getElementById('pb').value = Math.round(qStar * 100);
render();
});
console.log(fmt('Game payoff matrices — A: %stringify% B:', { stringify: JSON.stringify(MA) }), JSON.stringify(MB));
console.log(fmt('Mixed Nash: p*=%pStar% q*=%qStar%', { pStar: pStar.toFixed(4), qStar: qStar.toFixed(4) }));
render();
<p class="hint">
A 2×2 coordination game. Player A picks Row, Player B picks Column.
Each cell shows A's / B's payoff. Drag the sliders to set each player's
mixing probability and find the point where
<b>neither player can improve unilaterally</b> — that’s a Nash equilibrium.
</p>
<div class="game-wrap">
<table class="payoff-table">
<thead>
<tr>
<th></th>
<th>B: Col 1 (<span id="bpct">50</span>%)</th>
<th>B: Col 2 (<span id="bpct2">50</span>%)</th>
</tr>
</thead>
<tbody>
<tr>
<td class="rlabel">A: Row 1 (<span id="apct">50</span>%)</td>
<td id="c00" class="cell">2 / 1</td>
<td id="c01" class="cell">0 / 0</td>
</tr>
<tr>
<td class="rlabel">A: Row 2 (<span id="apct2">50</span>%)</td>
<td id="c10" class="cell">0 / 0</td>
<td id="c11" class="cell">1 / 2</td>
</tr>
</tbody>
</table>
<div class="sliders">
<label>Player A plays Row 1 with probability <b id="pa-val">0.50</b>
<input type="range" id="pa" min="0" max="100" value="50">
</label>
<label>Player B plays Col 1 with probability <b id="pb-val">0.50</b>
<input type="range" id="pb" min="0" max="100" value="50">
</label>
</div>
<div class="payoffs">
<div class="pblock" id="pa-block">
<span class="plabel">A's expected payoff</span>
<span class="pnum" id="pa-payoff">–</span>
</div>
<div class="pblock" id="pb-block">
<span class="plabel">B's expected payoff</span>
<span class="pnum" id="pb-payoff">–</span>
</div>
</div>
<div class="status" id="status">Adjust the sliders to search for a Nash equilibrium.</div>
<button id="findNash" type="button">Find Nash Equilibrium</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.game-wrap { display: flex; flex-direction: column; gap: .7rem; }
.payoff-table { border-collapse: collapse; font-size: .85rem; }
.payoff-table th, .payoff-table td { border: 1px solid #cdd9e3; padding: .4rem .7rem; text-align: center; }
.payoff-table th { background: #e8eef3; font-weight: 600; }
.rlabel { background: #e8eef3; font-weight: 600; text-align: left; white-space: nowrap; font-size: .82rem; }
.cell { font: 700 14px ui-monospace, monospace; min-width: 70px; transition: background .25s; }
.cell.hi-a { background: #dbe9ff; }
.cell.hi-b { background: #fde8c0; }
.cell.hi-ab { background: #d4f0da; }
.sliders { display: flex; flex-direction: column; gap: .5rem; }
.sliders label { display: flex; flex-direction: column; gap: .2rem; font-size: .88rem; }
.sliders input[type=range] { width: 100%; accent-color: #1d3557; }
.payoffs { display: flex; gap: 1rem; flex-wrap: wrap; }
.pblock { display: flex; flex-direction: column; align-items: center; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .4rem .8rem; min-width: 140px; }
.plabel { font-size: .75rem; color: #555; }
.pnum { font: 700 1.3rem ui-monospace, monospace; color: #1d3557; }
.pblock.nash { background: #d4f0da; border-color: #4caf50; }
.pblock.nash .pnum { color: #0a7d33; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.4em; color: #444; line-height: 1.4; }
.status.ok { color: #0a7d33; }
button { font: 600 14px system-ui; padding: .45rem 1rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// 2x2 coordination game — two pure Nash equilibria + one mixed Nash equilibrium.
// A: [[2,0],[0,1]] B: [[1,0],[0,2]]
// Pure Nash: (Row1,Col1) and (Row2,Col2).
// Mixed Nash: p* = 2/3, q* = 1/3 (found by making each player indifferent).
//
// B's Col-1 vs Col-2 payoff at row-mix p:
// Col1: B[0][0]*p + B[1][0]*(1-p) = 1*p + 0 = p
// Col2: B[0][1]*p + B[1][1]*(1-p) = 0 + 2*(1-p) = 2-2p
// Indiff: p = 2-2p => 3p=2 => p*=2/3
//
// A's Row-1 vs Row-2 payoff at col-mix q:
// Row1: A[0][0]*q + A[0][1]*(1-q) = 2q
// Row2: A[1][0]*q + A[1][1]*(1-q) = 0 + 1-q = 1-q
// Indiff: 2q = 1-q => 3q=1 => q*=1/3
const MA = [[2,0],[0,1]];
const MB = [[1,0],[0,2]];
// Compute Nash: p* such that B is indifferent, q* such that A is indifferent.
const dB_col = MB[0][0] - MB[1][0] - MB[0][1] + MB[1][1]; // 1-0-0+2=3
const pStar = dB_col !== 0 ? (MB[1][1] - MB[1][0]) / dB_col : 0.5; // (2-0)/3 = 2/3
const dA_row = MA[0][0] - MA[0][1] - MA[1][0] + MA[1][1]; // 2-0-0+1=3
const qStar = dA_row !== 0 ? (MA[1][1] - MA[0][1]) / dA_row : 0.5; // (1-0)/3 = 1/3
// Inline verification (runs at page load — open browser console to see)
console.assert(Math.abs(pStar - 2/3) < 1e-9, fmt('pStar should be 2/3, got %pStar%', { pStar: pStar }));
console.assert(Math.abs(qStar - 1/3) < 1e-9, fmt('qStar should be 1/3, got %qStar%', { qStar: qStar }));
function payoffA(p, q) {
return MA[0][0]*p*q + MA[0][1]*p*(1-q) + MA[1][0]*(1-p)*q + MA[1][1]*(1-p)*(1-q);
}
function payoffB(p, q) {
return MB[0][0]*p*q + MB[0][1]*p*(1-q) + MB[1][0]*(1-p)*q + MB[1][1]*(1-p)*(1-q);
}
function isNash(p, q, eps) {
const aAtP = payoffA(p, q);
const bAtQ = payoffB(p, q);
return aAtP >= payoffA(0, q) - eps && aAtP >= payoffA(1, q) - eps &&
bAtQ >= payoffB(p, 0) - eps && bAtQ >= payoffB(p, 1) - eps;
}
// Full Nash check (both assertions)
console.assert(isNash(pStar, qStar, 1e-9), 'Mixed Nash should satisfy isNash');
console.assert(isNash(1, 1, 1e-9), 'Pure Nash (Row1,Col1) should hold');
console.assert(isNash(0, 0, 1e-9), 'Pure Nash (Row2,Col2) should hold');
function render() {
const p = document.getElementById('pa').value / 100;
const q = document.getElementById('pb').value / 100;
document.getElementById('pa-val').textContent = p.toFixed(2);
document.getElementById('pb-val').textContent = q.toFixed(2);
document.getElementById('apct').textContent = Math.round(p * 100);
document.getElementById('apct2').textContent = Math.round((1-p) * 100);
document.getElementById('bpct').textContent = Math.round(q * 100);
document.getElementById('bpct2').textContent = Math.round((1-q) * 100);
const eA = payoffA(p, q);
const eB = payoffB(p, q);
document.getElementById('pa-payoff').textContent = eA.toFixed(3);
document.getElementById('pb-payoff').textContent = eB.toFixed(3);
// Highlight best-response cells
const cells = [['c00',0,0],['c01',0,1],['c10',1,0],['c11',1,1]];
const bestArow = payoffA(1,q) >= payoffA(0,q) ? 0 : 1;
const bestBcol = payoffB(p,1) >= payoffB(p,0) ? 1 : 0;
cells.forEach(([id, r, c]) => {
const el = document.getElementById(id);
el.className = 'cell';
const brA = r === bestArow;
const brB = c === bestBcol;
if (brA && brB) el.classList.add('hi-ab');
else if (brA) el.classList.add('hi-a');
else if (brB) el.classList.add('hi-b');
});
const nash = isNash(p, q, 0.06);
document.getElementById('pa-block').classList.toggle('nash', nash);
document.getElementById('pb-block').classList.toggle('nash', nash);
const status = document.getElementById('status');
if (nash) {
status.textContent = '✓ Nash equilibrium! Neither player benefits from changing their mix alone.';
status.className = 'status ok';
} else {
const aGain = Math.max(payoffA(0,q), payoffA(1,q)) - eA;
const bGain = Math.max(payoffB(p,0), payoffB(p,1)) - eB;
const who = (aGain > bGain + 0.001) ? 'Player A' : (bGain > aGain + 0.001 ? 'Player B' : 'Both players');
status.textContent = fmt('Not a Nash equilibrium — %who% could gain by changing strategy.', { who: who });
status.className = 'status';
}
}
document.getElementById('pa').addEventListener('input', render);
document.getElementById('pb').addEventListener('input', render);
document.getElementById('findNash').addEventListener('click', () => {
document.getElementById('pa').value = Math.round(pStar * 100);
document.getElementById('pb').value = Math.round(qStar * 100);
render();
});
console.log(fmt('Game payoff matrices — A: %stringify% B:', { stringify: JSON.stringify(MA) }), JSON.stringify(MB));
console.log(fmt('Mixed Nash: p*=%pStar% q*=%qStar%', { pStar: pStar.toFixed(4), qStar: qStar.toFixed(4) }));
render();
Drag the sliders to set each player's mixing probability. The demo computes each player's expected payoff and highlights when neither player can improve by unilaterally shifting their mix — that is a Nash equilibrium. Notice how finding this balance point requires searching a continuous space, just as Brouwer's fixed-point theorem requires searching a convex set for a fixed point.
Comments
Loading comments...