In the demo below, you play repeated rounds of the prisoner's dilemma against a grim-trigger opponent. Grim trigger cooperates as long as you cooperate — but the moment you defect once, it defects forever.
<div class="controls">
<label>Discount factor δ: <span id="delta-val">0.90</span>
<input type="range" id="delta" min="0.50" max="0.99" step="0.01" value="0.90">
</label>
<button id="coop-btn" type="button">Cooperate</button>
<button id="defect-btn" type="button" class="defect">Defect</button>
<button id="reset-btn" type="button" class="ghost">Restart</button>
</div>
<div class="payoff-table">
<div class="pt-title">Payoff matrix (you / opponent)</div>
<table>
<thead><tr><th></th><th>Opp: Cooperate</th><th>Opp: Defect</th></tr></thead>
<tbody>
<tr><th>You: Cooperate</th><td id="cc" class="hi">3 / 3</td><td id="cd">0 / 5</td></tr>
<tr><th>You: Defect</th><td id="dc">5 / 0</td><td id="dd">1 / 1</td></tr>
</tbody>
</table>
</div>
<div class="history" id="history"></div>
<div class="stats">
<div class="stat"><span class="label">Your total score</span><span id="my-score">0</span></div>
<div class="stat"><span class="label">Opponent total score</span><span id="opp-score">0</span></div>
<div class="stat"><span class="label">Round</span><span id="round">0</span></div>
<div class="stat eq-status"><span class="label">Coop. equilibrium?</span><span id="eq-status">–</span></div>
</div>
<div class="theorem-box" id="theorem-box"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .8rem; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; white-space: nowrap; }
input[type=range] { width: 120px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.defect { background: #c92f3c; border-color: #a8202c; }
button.ghost { background: #fff; color: #555; border-color: #bbb; }
.payoff-table { margin-bottom: .7rem; }
.pt-title { font-size: .78rem; color: #666; margin-bottom: .3rem; }
table { border-collapse: collapse; font-size: .82rem; }
th { font-weight: 600; padding: .3rem .55rem; background: #f0f4f8; border: 1px solid #cdd9e3; }
td { padding: .3rem .55rem; border: 1px solid #cdd9e3; text-align: center; }
td.hi { background: #d4f0de; font-weight: 700; }
.history { min-height: 56px; max-height: 80px; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .7rem; align-content: flex-start; }
.round-chip { padding: 2px 7px; border-radius: 12px; font-size: .78rem; font-weight: 600; }
.chip-cc { background: #d4f0de; color: #0a7d33; }
.chip-cd { background: #fde8e9; color: #9b1f2a; }
.chip-dc { background: #fff0cc; color: #7d5800; }
.chip-dd { background: #ebe9e9; color: #555; }
.stats { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .6rem; }
.stat { display: flex; flex-direction: column; align-items: center; background: #f6f8fb; border: 1px solid #dde3ea; border-radius: 8px; padding: .35rem .65rem; min-width: 80px; }
.stat .label { font-size: .7rem; color: #666; text-align: center; }
.stat span:last-child { font-size: 1.1rem; font-weight: 700; }
.eq-status span:last-child.yes { color: #0a7d33; }
.eq-status span:last-child.no { color: #c92f3c; }
.theorem-box { background: #eef3fb; border-left: 4px solid #1d3557; border-radius: 6px; padding: .5rem .75rem; font-size: .82rem; line-height: 1.5; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Repeated Prisoner’s Dilemma with Grim Trigger opponent.
// Payoffs: CC=3/3 CD=0/5 DC=5/0 DD=1/1
// R=3 (reward), T=5 (temptation), P=1 (punishment), S=0 (sucker)
// Folk theorem: cooperation is subgame-perfect equilibrium iff delta >= (T-R)/(T-P)
// => delta >= (5-3)/(5-1) = 2/4 = 0.5
const R=3, T=5, P=1; // S=0 unused in threshold formula
const THRESHOLD = (T - R) / (T - P); // = 0.5
let myScore = 0, oppScore = 0, round = 0, delta = 0.90;
let grimTriggered = false; // once true, opponent defects forever
function getDelta(){ return parseFloat(document.getElementById('delta').value); }
function updateTheoremBox(){
const d = getDelta();
const eq = d >= THRESHOLD;
const box = document.getElementById('theorem-box');
box.innerHTML = fmt(`<b>Folk theorem check:</b> cooperation is an equilibrium when
δ ≥ (T−R)/(T−P) = (%T%−%R%)/(%T%−%P%) = <b>%THRESHOLD%</b>.
Current δ = <b>%d%</b> → cooperation %v%.`, { T: T, R: R, P: P, THRESHOLD: THRESHOLD.toFixed(2), d: d.toFixed(2), v: eq
? '<span style="color:#0a7d33;font-weight:700">IS an equilibrium ✓</span>'
: '<span style="color:#c92f3c;font-weight:700">is NOT an equilibrium ✗</span>' });
const eqEl = document.getElementById('eq-status');
eqEl.textContent = eq ? `Yes` : `No`;
eqEl.className = eq ? 'yes' : 'no';
}
function addChip(code, text){
const h = document.getElementById('history');
const chip = document.createElement('span');
chip.className = 'round-chip chip-' + code;
chip.textContent = text;
h.appendChild(chip);
h.scrollTop = h.scrollHeight;
}
function play(myMove){
round++;
// Grim trigger: cooperate until first defection, then defect forever.
const oppMove = grimTriggered ? 'D' : 'C';
if(myMove === 'D') grimTriggered = true;
let myR, oppR, code;
if(myMove==='C' && oppMove==='C'){ myR=R; oppR=R; code='cc'; }
else if(myMove==='C' && oppMove==='D'){ myR=0; oppR=T; code='cd'; }
else if(myMove==='D' && oppMove==='C'){ myR=T; oppR=0; code='dc'; }
else { myR=P; oppR=P; code='dd'; }
myScore += myR; oppScore += oppR;
addChip(code, `R${round}: ${myMove}/${oppMove} (${myR}/${oppR})`);
document.getElementById('my-score').textContent = myScore;
document.getElementById('opp-score').textContent = oppScore;
document.getElementById('round').textContent = round;
// Highlight active cell
['cc','cd','dc','dd'].forEach(id => document.getElementById(id).classList.remove('hi'));
document.getElementById(code).classList.add('hi');
updateTheoremBox();
// Disable cooperation if grim triggered
if(grimTriggered){
document.getElementById('coop-btn').disabled = true;
document.getElementById('coop-btn').title = `Opponent now defects forever (grim trigger activated)`;
}
}
function reset(){
myScore=0; oppScore=0; round=0; grimTriggered=false;
document.getElementById('my-score').textContent='0';
document.getElementById('opp-score').textContent='0';
document.getElementById('round').textContent='0';
document.getElementById('history').innerHTML='';
document.getElementById('coop-btn').disabled=false;
document.getElementById('coop-btn').title='';
['cc','cd','dc','dd'].forEach(id => document.getElementById(id).classList.remove('hi'));
document.getElementById('cc').classList.add('hi');
updateTheoremBox();
}
document.getElementById('delta').addEventListener('input', function(){
document.getElementById('delta-val').textContent = parseFloat(this.value).toFixed(2);
updateTheoremBox();
});
document.getElementById('coop-btn').addEventListener('click', ()=>play('C'));
document.getElementById('defect-btn').addEventListener('click', ()=>play('D'));
document.getElementById('reset-btn').addEventListener('click', reset);
reset();
// Verify: folk theorem threshold for these payoffs = 0.5
console.assert(Math.abs(THRESHOLD - 0.5) < 1e-9, 'Threshold should be 0.5');
console.log(`Folk theorem threshold (T-R)/(T-P) =`, THRESHOLD, `-- verified correct`);
<div class="controls">
<label>Discount factor δ: <span id="delta-val">0.90</span>
<input type="range" id="delta" min="0.50" max="0.99" step="0.01" value="0.90">
</label>
<button id="coop-btn" type="button">Cooperate</button>
<button id="defect-btn" type="button" class="defect">Defect</button>
<button id="reset-btn" type="button" class="ghost">Restart</button>
</div>
<div class="payoff-table">
<div class="pt-title">Payoff matrix (you / opponent)</div>
<table>
<thead><tr><th></th><th>Opp: Cooperate</th><th>Opp: Defect</th></tr></thead>
<tbody>
<tr><th>You: Cooperate</th><td id="cc" class="hi">3 / 3</td><td id="cd">0 / 5</td></tr>
<tr><th>You: Defect</th><td id="dc">5 / 0</td><td id="dd">1 / 1</td></tr>
</tbody>
</table>
</div>
<div class="history" id="history"></div>
<div class="stats">
<div class="stat"><span class="label">Your total score</span><span id="my-score">0</span></div>
<div class="stat"><span class="label">Opponent total score</span><span id="opp-score">0</span></div>
<div class="stat"><span class="label">Round</span><span id="round">0</span></div>
<div class="stat eq-status"><span class="label">Coop. equilibrium?</span><span id="eq-status">–</span></div>
</div>
<div class="theorem-box" id="theorem-box"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; margin-bottom: .8rem; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .85rem; white-space: nowrap; }
input[type=range] { width: 120px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border-radius: 7px; cursor: pointer; border: 1px solid #1d3557; background: #1d3557; color: #fff; }
button.defect { background: #c92f3c; border-color: #a8202c; }
button.ghost { background: #fff; color: #555; border-color: #bbb; }
.payoff-table { margin-bottom: .7rem; }
.pt-title { font-size: .78rem; color: #666; margin-bottom: .3rem; }
table { border-collapse: collapse; font-size: .82rem; }
th { font-weight: 600; padding: .3rem .55rem; background: #f0f4f8; border: 1px solid #cdd9e3; }
td { padding: .3rem .55rem; border: 1px solid #cdd9e3; text-align: center; }
td.hi { background: #d4f0de; font-weight: 700; }
.history { min-height: 56px; max-height: 80px; overflow-y: auto; display: flex; flex-wrap: wrap; gap: 3px; margin-bottom: .7rem; align-content: flex-start; }
.round-chip { padding: 2px 7px; border-radius: 12px; font-size: .78rem; font-weight: 600; }
.chip-cc { background: #d4f0de; color: #0a7d33; }
.chip-cd { background: #fde8e9; color: #9b1f2a; }
.chip-dc { background: #fff0cc; color: #7d5800; }
.chip-dd { background: #ebe9e9; color: #555; }
.stats { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .6rem; }
.stat { display: flex; flex-direction: column; align-items: center; background: #f6f8fb; border: 1px solid #dde3ea; border-radius: 8px; padding: .35rem .65rem; min-width: 80px; }
.stat .label { font-size: .7rem; color: #666; text-align: center; }
.stat span:last-child { font-size: 1.1rem; font-weight: 700; }
.eq-status span:last-child.yes { color: #0a7d33; }
.eq-status span:last-child.no { color: #c92f3c; }
.theorem-box { background: #eef3fb; border-left: 4px solid #1d3557; border-radius: 6px; padding: .5rem .75rem; font-size: .82rem; line-height: 1.5; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Repeated Prisoner’s Dilemma with Grim Trigger opponent.
// Payoffs: CC=3/3 CD=0/5 DC=5/0 DD=1/1
// R=3 (reward), T=5 (temptation), P=1 (punishment), S=0 (sucker)
// Folk theorem: cooperation is subgame-perfect equilibrium iff delta >= (T-R)/(T-P)
// => delta >= (5-3)/(5-1) = 2/4 = 0.5
const R=3, T=5, P=1; // S=0 unused in threshold formula
const THRESHOLD = (T - R) / (T - P); // = 0.5
let myScore = 0, oppScore = 0, round = 0, delta = 0.90;
let grimTriggered = false; // once true, opponent defects forever
function getDelta(){ return parseFloat(document.getElementById('delta').value); }
function updateTheoremBox(){
const d = getDelta();
const eq = d >= THRESHOLD;
const box = document.getElementById('theorem-box');
box.innerHTML = fmt(`<b>Folk theorem check:</b> cooperation is an equilibrium when
δ ≥ (T−R)/(T−P) = (%T%−%R%)/(%T%−%P%) = <b>%THRESHOLD%</b>.
Current δ = <b>%d%</b> → cooperation %v%.`, { T: T, R: R, P: P, THRESHOLD: THRESHOLD.toFixed(2), d: d.toFixed(2), v: eq
? '<span style="color:#0a7d33;font-weight:700">IS an equilibrium ✓</span>'
: '<span style="color:#c92f3c;font-weight:700">is NOT an equilibrium ✗</span>' });
const eqEl = document.getElementById('eq-status');
eqEl.textContent = eq ? `Yes` : `No`;
eqEl.className = eq ? 'yes' : 'no';
}
function addChip(code, text){
const h = document.getElementById('history');
const chip = document.createElement('span');
chip.className = 'round-chip chip-' + code;
chip.textContent = text;
h.appendChild(chip);
h.scrollTop = h.scrollHeight;
}
function play(myMove){
round++;
// Grim trigger: cooperate until first defection, then defect forever.
const oppMove = grimTriggered ? 'D' : 'C';
if(myMove === 'D') grimTriggered = true;
let myR, oppR, code;
if(myMove==='C' && oppMove==='C'){ myR=R; oppR=R; code='cc'; }
else if(myMove==='C' && oppMove==='D'){ myR=0; oppR=T; code='cd'; }
else if(myMove==='D' && oppMove==='C'){ myR=T; oppR=0; code='dc'; }
else { myR=P; oppR=P; code='dd'; }
myScore += myR; oppScore += oppR;
addChip(code, `R${round}: ${myMove}/${oppMove} (${myR}/${oppR})`);
document.getElementById('my-score').textContent = myScore;
document.getElementById('opp-score').textContent = oppScore;
document.getElementById('round').textContent = round;
// Highlight active cell
['cc','cd','dc','dd'].forEach(id => document.getElementById(id).classList.remove('hi'));
document.getElementById(code).classList.add('hi');
updateTheoremBox();
// Disable cooperation if grim triggered
if(grimTriggered){
document.getElementById('coop-btn').disabled = true;
document.getElementById('coop-btn').title = `Opponent now defects forever (grim trigger activated)`;
}
}
function reset(){
myScore=0; oppScore=0; round=0; grimTriggered=false;
document.getElementById('my-score').textContent='0';
document.getElementById('opp-score').textContent='0';
document.getElementById('round').textContent='0';
document.getElementById('history').innerHTML='';
document.getElementById('coop-btn').disabled=false;
document.getElementById('coop-btn').title='';
['cc','cd','dc','dd'].forEach(id => document.getElementById(id).classList.remove('hi'));
document.getElementById('cc').classList.add('hi');
updateTheoremBox();
}
document.getElementById('delta').addEventListener('input', function(){
document.getElementById('delta-val').textContent = parseFloat(this.value).toFixed(2);
updateTheoremBox();
});
document.getElementById('coop-btn').addEventListener('click', ()=>play('C'));
document.getElementById('defect-btn').addEventListener('click', ()=>play('D'));
document.getElementById('reset-btn').addEventListener('click', reset);
reset();
// Verify: folk theorem threshold for these payoffs = 0.5
console.assert(Math.abs(THRESHOLD - 0.5) < 1e-9, 'Threshold should be 0.5');
console.log(`Folk theorem threshold (T-R)/(T-P) =`, THRESHOLD, `-- verified correct`);
Adjust the discount factor δ (how much you value tomorrow versus today). The folk theorem predicts cooperation is an equilibrium when δ≥T−PT−R, where R is the reward for mutual cooperation, T is the temptation to defect, and P is the punishment for mutual defection. Watch the running payoffs to feel when defection stops being worth it.
Comments
Loading comments...