Below is a 2×2 game. Each cell shows the payoffs (Row, Column). A cell is a pure Nash equilibrium when neither player can switch their own move and earn more. Click a cell to test it — the demo highlights whether either player wants to deviate.
<p class="hint">Each cell shows payoffs <b>(Row, Column)</b>. A cell is a <b>pure Nash equilibrium</b> when neither player can switch their own move and earn more. Click a cell to test it. Checking is instant — finding all of them is the interesting part.</p>
<table class="game">
<tr><th></th><th class="lbl">C: Left</th><th class="lbl">C: Right</th></tr>
<tr><th class="lbl">R: Top</th><td class="cell" data-r="0" data-c="0"></td><td class="cell" data-r="0" data-c="1"></td></tr>
<tr><th class="lbl">R: Bottom</th><td class="cell" data-r="1" data-c="0"></td><td class="cell" data-r="1" data-c="1"></td></tr>
</table>
<div class="status" id="status">Click a cell to test it for stability.</div>
<div class="btns">
<button id="findall" type="button">Find all pure equilibria</button>
<button id="brd" type="button">Best-response dynamics</button>
<button id="shuffle" type="button" class="ghost">New random game</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.game { border-collapse: collapse; margin: .3rem 0 .6rem; }
.game th, .game td { width: 92px; height: 64px; text-align: center; }
.lbl { font: 600 13px system-ui, sans-serif; color: #1d3557; }
.cell { font: 700 16px ui-monospace, monospace; background: #e8eef3;
border: 2px solid #cdd9e3; border-radius: 8px; cursor: pointer;
transition: all .12s; }
.cell:hover { background: #dce6ee; }
.cell.sel { border-color: #1d3557; }
.cell.eq { background: #d4f4dd; border-color: #0a7d33; }
.cell.no { background: #fbe0e3; border-color: #c92f3c; }
.cell.mark::after { content: " ★"; color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 2.6em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A 2x2 game: P[r][c] = [rowPayoff, colPayoff]. A cell (r,c) is a PURE Nash
// equilibrium iff the Row player can't do better by switching row (given c)
// AND the Column player can't do better by switching column (given r).
// Start with the classic "coordination" game: two pure equilibria on the diagonal.
let P = [
[[3,3],[0,2]],
[[2,0],[1,1]]
];
function rndGame(){
const v=()=>Math.floor(Math.random()*4); // payoffs 0..3
return [[[v(),v()],[v(),v()]],[[v(),v()],[v(),v()]]];
}
// Is cell (r,c) a pure Nash equilibrium?
function isEq(P,r,c){
const or=1-r, oc=1-c;
const rowOk = P[r][c][0] >= P[or][c][0]; // Row can't gain by switching row
const colOk = P[r][c][1] >= P[r][oc][1]; // Col can't gain by switching col
return rowOk && colOk;
}
function allEq(P){
const res=[];
for(let r=0;r<2;r++)for(let c=0;c<2;c++) if(isEq(P,r,c)) res.push([r,c]);
return res;
}
function paint(){
document.querySelectorAll('.cell').forEach(td=>{
const r=+td.dataset.r, c=+td.dataset.c;
td.textContent='('+P[r][c][0]+', '+P[r][c][1]+')';
td.className='cell';
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.innerHTML=t; s.className='status '+(cls||''); }
function testCell(r,c){
paint();
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
const or=1-r, oc=1-c;
const rowOk = P[r][c][0] >= P[or][c][0];
const colOk = P[r][c][1] >= P[r][oc][1];
if(rowOk && colOk){
td.classList.add('eq');
setStatus('★ Stable! Neither player gains by switching alone — this is a pure Nash equilibrium.','ok');
} else {
td.classList.add('no');
let who=[];
if(!rowOk) who.push(fmt('Row would switch (%v%→%v2%)', { v: P[r][c][0], v2: P[or][c][0] }));
if(!colOk) who.push(fmt('Column would switch (%v%→%v2%)', { v: P[r][c][1], v2: P[r][oc][1] }));
setStatus(fmt('✗ Not stable. %who%.', { who: who.join('; ') }),'bad');
}
}
function findAll(){
paint();
const eqs=allEq(P);
eqs.forEach(([r,c])=>{
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
td.classList.add('eq','mark');
});
if(eqs.length===0)
setStatus('No <i>pure</i> equilibrium here — yet Nash guarantees a <b>mixed</b> one exists (players randomize). Finding it has no easy recipe.','bad');
else
setStatus(eqs.length>1 ? fmt('Found %length% pure equilibria (★) by checking all 4 cells. Easy here; for big games this search explodes.', { length: eqs.length }) : fmt('Found %length% pure equilibrium (★) by checking all 4 cells. Easy here; for big games this search explodes.', { length: eqs.length }),'ok');
}
// Best-response dynamics: players take turns. Each step, one player jumps to
// their best reply to the other's current move. With a pure equilibrium this settles; without one (e.g. matching pennies) it cycles forever.
function bestRespond(){
paint();
let r=Math.floor(Math.random()*2), c=Math.floor(Math.random()*2);
const path=['('+(r?'B':'T')+','+(c?'R':'L')+')']; let stable=false;
for(let step=0; step<12; step++){
if(step%2===0){ // Row best-responds to c
const nr = (P[1][c][0] > P[0][c][0]) ? 1 : 0;
if(nr===r && isEq(P,r,c)){ stable=true; break; }
r=nr;
} else { // Col best-responds to r
const nc = (P[r][1][1] > P[r][0][1]) ? 1 : 0;
if(nc===c && isEq(P,r,c)){ stable=true; break; }
c=nc;
}
path.push('('+(r?'B':'T')+','+(c?'R':'L')+')');
if(isEq(P,r,c)){ stable=true; break; }
}
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
td.classList.add(isEq(P,r,c)?'eq':'sel');
if(stable && isEq(P,r,c))
setStatus(fmt('Path: %path%. Converged to a pure equilibrium. When one exists, taking turns settles down.', { path: path.join(' → ') }),'ok');
else
setStatus(fmt('Path: %path%. No convergence: best replies chase each other in a loop. No pure equilibrium here — yet a <b>mixed</b> one is still guaranteed, just not reachable this way.', { path: path.join(' → ') }),'bad');
}
document.querySelectorAll('.cell').forEach(td=>{
td.addEventListener('click',()=>testCell(+td.dataset.r,+td.dataset.c));
});
document.getElementById('findall').addEventListener('click',findAll);
document.getElementById('brd').addEventListener('click',bestRespond);
document.getElementById('shuffle').addEventListener('click',()=>{ P=rndGame(); paint(); setStatus('New random game. Click a cell or press "Find all".',''); });
paint();
<p class="hint">Each cell shows payoffs <b>(Row, Column)</b>. A cell is a <b>pure Nash equilibrium</b> when neither player can switch their own move and earn more. Click a cell to test it. Checking is instant — finding all of them is the interesting part.</p>
<table class="game">
<tr><th></th><th class="lbl">C: Left</th><th class="lbl">C: Right</th></tr>
<tr><th class="lbl">R: Top</th><td class="cell" data-r="0" data-c="0"></td><td class="cell" data-r="0" data-c="1"></td></tr>
<tr><th class="lbl">R: Bottom</th><td class="cell" data-r="1" data-c="0"></td><td class="cell" data-r="1" data-c="1"></td></tr>
</table>
<div class="status" id="status">Click a cell to test it for stability.</div>
<div class="btns">
<button id="findall" type="button">Find all pure equilibria</button>
<button id="brd" type="button">Best-response dynamics</button>
<button id="shuffle" type="button" class="ghost">New random game</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.game { border-collapse: collapse; margin: .3rem 0 .6rem; }
.game th, .game td { width: 92px; height: 64px; text-align: center; }
.lbl { font: 600 13px system-ui, sans-serif; color: #1d3557; }
.cell { font: 700 16px ui-monospace, monospace; background: #e8eef3;
border: 2px solid #cdd9e3; border-radius: 8px; cursor: pointer;
transition: all .12s; }
.cell:hover { background: #dce6ee; }
.cell.sel { border-color: #1d3557; }
.cell.eq { background: #d4f4dd; border-color: #0a7d33; }
.cell.no { background: #fbe0e3; border-color: #c92f3c; }
.cell.mark::after { content: " ★"; color: #0a7d33; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 2.6em; line-height: 1.4; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// A 2x2 game: P[r][c] = [rowPayoff, colPayoff]. A cell (r,c) is a PURE Nash
// equilibrium iff the Row player can't do better by switching row (given c)
// AND the Column player can't do better by switching column (given r).
// Start with the classic "coordination" game: two pure equilibria on the diagonal.
let P = [
[[3,3],[0,2]],
[[2,0],[1,1]]
];
function rndGame(){
const v=()=>Math.floor(Math.random()*4); // payoffs 0..3
return [[[v(),v()],[v(),v()]],[[v(),v()],[v(),v()]]];
}
// Is cell (r,c) a pure Nash equilibrium?
function isEq(P,r,c){
const or=1-r, oc=1-c;
const rowOk = P[r][c][0] >= P[or][c][0]; // Row can't gain by switching row
const colOk = P[r][c][1] >= P[r][oc][1]; // Col can't gain by switching col
return rowOk && colOk;
}
function allEq(P){
const res=[];
for(let r=0;r<2;r++)for(let c=0;c<2;c++) if(isEq(P,r,c)) res.push([r,c]);
return res;
}
function paint(){
document.querySelectorAll('.cell').forEach(td=>{
const r=+td.dataset.r, c=+td.dataset.c;
td.textContent='('+P[r][c][0]+', '+P[r][c][1]+')';
td.className='cell';
});
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.innerHTML=t; s.className='status '+(cls||''); }
function testCell(r,c){
paint();
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
const or=1-r, oc=1-c;
const rowOk = P[r][c][0] >= P[or][c][0];
const colOk = P[r][c][1] >= P[r][oc][1];
if(rowOk && colOk){
td.classList.add('eq');
setStatus('★ Stable! Neither player gains by switching alone — this is a pure Nash equilibrium.','ok');
} else {
td.classList.add('no');
let who=[];
if(!rowOk) who.push(fmt('Row would switch (%v%→%v2%)', { v: P[r][c][0], v2: P[or][c][0] }));
if(!colOk) who.push(fmt('Column would switch (%v%→%v2%)', { v: P[r][c][1], v2: P[r][oc][1] }));
setStatus(fmt('✗ Not stable. %who%.', { who: who.join('; ') }),'bad');
}
}
function findAll(){
paint();
const eqs=allEq(P);
eqs.forEach(([r,c])=>{
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
td.classList.add('eq','mark');
});
if(eqs.length===0)
setStatus('No <i>pure</i> equilibrium here — yet Nash guarantees a <b>mixed</b> one exists (players randomize). Finding it has no easy recipe.','bad');
else
setStatus(eqs.length>1 ? fmt('Found %length% pure equilibria (★) by checking all 4 cells. Easy here; for big games this search explodes.', { length: eqs.length }) : fmt('Found %length% pure equilibrium (★) by checking all 4 cells. Easy here; for big games this search explodes.', { length: eqs.length }),'ok');
}
// Best-response dynamics: players take turns. Each step, one player jumps to
// their best reply to the other's current move. With a pure equilibrium this settles; without one (e.g. matching pennies) it cycles forever.
function bestRespond(){
paint();
let r=Math.floor(Math.random()*2), c=Math.floor(Math.random()*2);
const path=['('+(r?'B':'T')+','+(c?'R':'L')+')']; let stable=false;
for(let step=0; step<12; step++){
if(step%2===0){ // Row best-responds to c
const nr = (P[1][c][0] > P[0][c][0]) ? 1 : 0;
if(nr===r && isEq(P,r,c)){ stable=true; break; }
r=nr;
} else { // Col best-responds to r
const nc = (P[r][1][1] > P[r][0][1]) ? 1 : 0;
if(nc===c && isEq(P,r,c)){ stable=true; break; }
c=nc;
}
path.push('('+(r?'B':'T')+','+(c?'R':'L')+')');
if(isEq(P,r,c)){ stable=true; break; }
}
const td=document.querySelector('.cell[data-r="'+r+'"][data-c="'+c+'"]');
td.classList.add(isEq(P,r,c)?'eq':'sel');
if(stable && isEq(P,r,c))
setStatus(fmt('Path: %path%. Converged to a pure equilibrium. When one exists, taking turns settles down.', { path: path.join(' → ') }),'ok');
else
setStatus(fmt('Path: %path%. No convergence: best replies chase each other in a loop. No pure equilibrium here — yet a <b>mixed</b> one is still guaranteed, just not reachable this way.', { path: path.join(' → ') }),'bad');
}
document.querySelectorAll('.cell').forEach(td=>{
td.addEventListener('click',()=>testCell(+td.dataset.r,+td.dataset.c));
});
document.getElementById('findall').addEventListener('click',findAll);
document.getElementById('brd').addEventListener('click',bestRespond);
document.getElementById('shuffle').addEventListener('click',()=>{ P=rndGame(); paint(); setStatus('New random game. Click a cell or press "Find all".',''); });
paint();
Notice the asymmetry. Checking a candidate cell is trivial: compare it against the two alternatives in its row and column. Finding an equilibrium — especially a mixed one, where players randomize — has no such shortcut. Press Best-response dynamics and watch the players chase each other's choices; sometimes they converge, sometimes they circle forever. For tiny games brute force works, but the search has no general fast recipe.
Comments
Loading comments...