Adjust the resource value V and injury cost C, then press Run to watch a population of 200 individuals evolve over generations. Each round every individual is paired at random with another; payoffs accumulate; low-fitness individuals are replaced by offspring of high-fitness ones (proportional selection with mutation).
<div class="controls">
<label>Resource value V: <span id="vval">4</span>
<input type="range" id="vslider" min="1" max="10" value="4" step="1">
</label>
<label>Injury cost C: <span id="cval">10</span>
<input type="range" id="cslider" min="2" max="20" value="10" step="1">
</label>
<div class="ess-display">ESS frequency (V/C): <span id="essfreq">0.400</span></div>
</div>
<canvas id="chart" width="480" height="200"></canvas>
<div class="pop-bar">
<div id="hawk-bar" class="hawk-seg"></div>
<div id="dove-bar" class="dove-seg"></div>
</div>
<div class="pop-labels">
<span class="hawk-label">Hawks: <span id="hawk-pct">50%</span></span>
<span class="dove-label">Doves: <span id="dove-pct">50%</span></span>
</div>
<div class="btns">
<button id="run" type="button">Run</button>
<button id="step" type="button">Step</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Press Run to start the simulation.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.controls input[type=range] { flex: 1; }
.ess-display { font-weight: 700; color: #1d3557; font-size: .9rem; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; }
.pop-bar { display: flex; height: 22px; border-radius: 6px; overflow: hidden; margin: .5rem 0 .2rem; border: 1px solid #cdd9e3; }
.hawk-seg { background: #e63946; transition: width .2s; }
.dove-seg { background: #457b9d; flex: 1; transition: width .2s; }
.pop-labels { display: flex; justify-content: space-between; font-size: .85rem; font-weight: 600; margin-bottom: .4rem; }
.hawk-label { color: #e63946; }
.dove-label { color: #457b9d; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; margin-top: .4rem; min-height: 1.3em; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hawk-Dove ESS simulation
// Payoff matrix (symmetric):
// HvH: each gets (V-C)/2 (50% win V, 50% lose C)
// HvD: Hawk gets V, Dove gets 0
// DvH: Dove gets 0, Hawk gets V [same as HvD by symmetry]
// DvD: each gets V/2
// ESS mixed frequency: p* = V/C (when V < C)
const POP = 200;
const GENS = 120;
const MUTATION = 0.01;
let V = 4, C = 10;
let hawks = 100; // count of hawks in population
let history = []; // fraction of hawks per generation
let running = false;
let timer = null;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
function essFreq() { return Math.min(V / C, 1); }
function payoff(isHawk, p) {
// expected payoff for a hawk (p = fraction hawks)
// f(H) = p*(V-C)/2 + (1-p)*V
// f(D) = p*0 + (1-p)*V/2
if (isHawk) return p * (V - C) / 2 + (1 - p) * V;
return (1 - p) * V / 2;
}
function step() {
const p = hawks / POP;
const fH = payoff(true, p);
const fD = payoff(false, p);
// Shift baseline so fitnesses are positive: add |min(fH,fD)| + 1
const base = Math.min(fH, fD);
const wH = fH - base + 1;
const wD = fD - base + 1;
// Total weight
const totalW = hawks * wH + (POP - hawks) * wD;
// Expected hawks next gen (Wright-Fisher style proportional selection)
let newHawks = Math.round(POP * (hawks * wH) / totalW);
// Mutation: each individual flips with probability MUTATION
const mutateToH = Math.round((POP - newHawks) * MUTATION);
const mutateToD = Math.round(newHawks * MUTATION);
newHawks = newHawks - mutateToD + mutateToH;
newHawks = Math.max(0, Math.min(POP, newHawks));
hawks = newHawks;
history.push(hawks / POP);
draw();
updateUI();
}
function draw() {
ctx.clearRect(0, 0, W, H);
const pad = { top: 16, bottom: 24, left: 36, right: 12 };
const iW = W - pad.left - pad.right;
const iH = H - pad.top - pad.bottom;
// Background grid
ctx.strokeStyle = '#dde5ec';
ctx.lineWidth = 0.8;
[0, 0.25, 0.5, 0.75, 1].forEach(y => {
const cy = pad.top + iH * (1 - y);
ctx.beginPath(); ctx.moveTo(pad.left, cy); ctx.lineTo(pad.left + iW, cy); ctx.stroke();
ctx.fillStyle = '#8a9aaa'; ctx.font = '10px system-ui';
ctx.fillText((y * 100).toFixed(0) + '%', 2, cy + 4);
});
// ESS dashed line
const essY = pad.top + iH * (1 - essFreq());
ctx.strokeStyle = '#e8a020';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(pad.left, essY); ctx.lineTo(pad.left + iW, essY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#e8a020'; ctx.font = 'bold 10px system-ui';
ctx.fillText('ESS', pad.left + iW - 26, essY - 4);
// History line (hawks fraction)
if (history.length < 2) return;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((v, i) => {
const x = pad.left + (i / (GENS - 1)) * iW;
const y = pad.top + iH * (1 - v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
function updateUI() {
const p = hawks / POP;
document.getElementById('hawk-pct').textContent = (p * 100).toFixed(1) + '%';
document.getElementById('dove-pct').textContent = ((1 - p) * 100).toFixed(1) + '%';
document.getElementById('hawk-bar').style.width = (p * 100).toFixed(1) + '%';
const gen = history.length;
const ess = essFreq();
if (gen >= GENS) {
const diff = Math.abs(p - ess);
document.getElementById('status').textContent =
fmt('Generation %gen% — converged to %v%% % Hawks (ESS = %v2%%, diff %v3%%)', { gen: gen, v: (p * 100).toFixed(1), v2: (ess * 100).toFixed(1), v3: (diff * 100).toFixed(1) });
} else {
document.getElementById('status').textContent =
fmt('Generation %gen% — Hawks: %v%%, ESS target: %v2%%', { gen: gen, v: (p * 100).toFixed(1), v2: (ess * 100).toFixed(1) });
}
}
function reset() {
clearTimeout(timer); running = false;
hawks = Math.round(POP * 0.5);
history = [];
document.getElementById('run').textContent = 'Run';
document.getElementById('status').textContent = 'Press Run to start the simulation.';
draw();
updateUI();
}
function runLoop() {
if (!running || history.length >= GENS) { running = false; document.getElementById('run').textContent = 'Run'; return; }
step();
timer = setTimeout(runLoop, 60);
}
document.getElementById('run').addEventListener('click', () => {
if (history.length >= GENS) reset();
running = !running;
document.getElementById('run').textContent = running ? 'Pause' : 'Run';
if (running) runLoop();
});
document.getElementById('step').addEventListener('click', () => {
if (history.length < GENS) { running = false; document.getElementById('run').textContent = 'Run'; step(); }
});
document.getElementById('reset').addEventListener('click', reset);
function syncSliders() {
V = +document.getElementById('vslider').value;
C = +document.getElementById('cslider').value;
if (V >= C) { C = V + 1; document.getElementById('cslider').value = C; }
document.getElementById('vval').textContent = V;
document.getElementById('cval').textContent = C;
document.getElementById('essfreq').textContent = essFreq().toFixed(3);
}
document.getElementById('vslider').addEventListener('input', () => { syncSliders(); reset(); });
document.getElementById('cslider').addEventListener('input', () => { syncSliders(); reset(); });
syncSliders();
reset();
<div class="controls">
<label>Resource value V: <span id="vval">4</span>
<input type="range" id="vslider" min="1" max="10" value="4" step="1">
</label>
<label>Injury cost C: <span id="cval">10</span>
<input type="range" id="cslider" min="2" max="20" value="10" step="1">
</label>
<div class="ess-display">ESS frequency (V/C): <span id="essfreq">0.400</span></div>
</div>
<canvas id="chart" width="480" height="200"></canvas>
<div class="pop-bar">
<div id="hawk-bar" class="hawk-seg"></div>
<div id="dove-bar" class="dove-seg"></div>
</div>
<div class="pop-labels">
<span class="hawk-label">Hawks: <span id="hawk-pct">50%</span></span>
<span class="dove-label">Doves: <span id="dove-pct">50%</span></span>
</div>
<div class="btns">
<button id="run" type="button">Run</button>
<button id="step" type="button">Step</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status">Press Run to start the simulation.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .6rem; font-size: .9rem; }
.controls label { display: flex; align-items: center; gap: .5rem; }
.controls input[type=range] { flex: 1; }
.ess-display { font-weight: 700; color: #1d3557; font-size: .9rem; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; border-radius: 8px; background: #f7f9fb; }
.pop-bar { display: flex; height: 22px; border-radius: 6px; overflow: hidden; margin: .5rem 0 .2rem; border: 1px solid #cdd9e3; }
.hawk-seg { background: #e63946; transition: width .2s; }
.dove-seg { background: #457b9d; flex: 1; transition: width .2s; }
.pop-labels { display: flex; justify-content: space-between; font-size: .85rem; font-weight: 600; margin-bottom: .4rem; }
.hawk-label { color: #e63946; }
.dove-label { color: #457b9d; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .4rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.status { font-size: .9rem; margin-top: .4rem; min-height: 1.3em; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Hawk-Dove ESS simulation
// Payoff matrix (symmetric):
// HvH: each gets (V-C)/2 (50% win V, 50% lose C)
// HvD: Hawk gets V, Dove gets 0
// DvH: Dove gets 0, Hawk gets V [same as HvD by symmetry]
// DvD: each gets V/2
// ESS mixed frequency: p* = V/C (when V < C)
const POP = 200;
const GENS = 120;
const MUTATION = 0.01;
let V = 4, C = 10;
let hawks = 100; // count of hawks in population
let history = []; // fraction of hawks per generation
let running = false;
let timer = null;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
function essFreq() { return Math.min(V / C, 1); }
function payoff(isHawk, p) {
// expected payoff for a hawk (p = fraction hawks)
// f(H) = p*(V-C)/2 + (1-p)*V
// f(D) = p*0 + (1-p)*V/2
if (isHawk) return p * (V - C) / 2 + (1 - p) * V;
return (1 - p) * V / 2;
}
function step() {
const p = hawks / POP;
const fH = payoff(true, p);
const fD = payoff(false, p);
// Shift baseline so fitnesses are positive: add |min(fH,fD)| + 1
const base = Math.min(fH, fD);
const wH = fH - base + 1;
const wD = fD - base + 1;
// Total weight
const totalW = hawks * wH + (POP - hawks) * wD;
// Expected hawks next gen (Wright-Fisher style proportional selection)
let newHawks = Math.round(POP * (hawks * wH) / totalW);
// Mutation: each individual flips with probability MUTATION
const mutateToH = Math.round((POP - newHawks) * MUTATION);
const mutateToD = Math.round(newHawks * MUTATION);
newHawks = newHawks - mutateToD + mutateToH;
newHawks = Math.max(0, Math.min(POP, newHawks));
hawks = newHawks;
history.push(hawks / POP);
draw();
updateUI();
}
function draw() {
ctx.clearRect(0, 0, W, H);
const pad = { top: 16, bottom: 24, left: 36, right: 12 };
const iW = W - pad.left - pad.right;
const iH = H - pad.top - pad.bottom;
// Background grid
ctx.strokeStyle = '#dde5ec';
ctx.lineWidth = 0.8;
[0, 0.25, 0.5, 0.75, 1].forEach(y => {
const cy = pad.top + iH * (1 - y);
ctx.beginPath(); ctx.moveTo(pad.left, cy); ctx.lineTo(pad.left + iW, cy); ctx.stroke();
ctx.fillStyle = '#8a9aaa'; ctx.font = '10px system-ui';
ctx.fillText((y * 100).toFixed(0) + '%', 2, cy + 4);
});
// ESS dashed line
const essY = pad.top + iH * (1 - essFreq());
ctx.strokeStyle = '#e8a020';
ctx.lineWidth = 1.5;
ctx.setLineDash([5, 4]);
ctx.beginPath(); ctx.moveTo(pad.left, essY); ctx.lineTo(pad.left + iW, essY); ctx.stroke();
ctx.setLineDash([]);
ctx.fillStyle = '#e8a020'; ctx.font = 'bold 10px system-ui';
ctx.fillText('ESS', pad.left + iW - 26, essY - 4);
// History line (hawks fraction)
if (history.length < 2) return;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((v, i) => {
const x = pad.left + (i / (GENS - 1)) * iW;
const y = pad.top + iH * (1 - v);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
function updateUI() {
const p = hawks / POP;
document.getElementById('hawk-pct').textContent = (p * 100).toFixed(1) + '%';
document.getElementById('dove-pct').textContent = ((1 - p) * 100).toFixed(1) + '%';
document.getElementById('hawk-bar').style.width = (p * 100).toFixed(1) + '%';
const gen = history.length;
const ess = essFreq();
if (gen >= GENS) {
const diff = Math.abs(p - ess);
document.getElementById('status').textContent =
fmt('Generation %gen% — converged to %v%% % Hawks (ESS = %v2%%, diff %v3%%)', { gen: gen, v: (p * 100).toFixed(1), v2: (ess * 100).toFixed(1), v3: (diff * 100).toFixed(1) });
} else {
document.getElementById('status').textContent =
fmt('Generation %gen% — Hawks: %v%%, ESS target: %v2%%', { gen: gen, v: (p * 100).toFixed(1), v2: (ess * 100).toFixed(1) });
}
}
function reset() {
clearTimeout(timer); running = false;
hawks = Math.round(POP * 0.5);
history = [];
document.getElementById('run').textContent = 'Run';
document.getElementById('status').textContent = 'Press Run to start the simulation.';
draw();
updateUI();
}
function runLoop() {
if (!running || history.length >= GENS) { running = false; document.getElementById('run').textContent = 'Run'; return; }
step();
timer = setTimeout(runLoop, 60);
}
document.getElementById('run').addEventListener('click', () => {
if (history.length >= GENS) reset();
running = !running;
document.getElementById('run').textContent = running ? 'Pause' : 'Run';
if (running) runLoop();
});
document.getElementById('step').addEventListener('click', () => {
if (history.length < GENS) { running = false; document.getElementById('run').textContent = 'Run'; step(); }
});
document.getElementById('reset').addEventListener('click', reset);
function syncSliders() {
V = +document.getElementById('vslider').value;
C = +document.getElementById('cslider').value;
if (V >= C) { C = V + 1; document.getElementById('cslider').value = C; }
document.getElementById('vval').textContent = V;
document.getElementById('cval').textContent = C;
document.getElementById('essfreq').textContent = essFreq().toFixed(3);
}
document.getElementById('vslider').addEventListener('input', () => { syncSliders(); reset(); });
document.getElementById('cslider').addEventListener('input', () => { syncSliders(); reset(); });
syncSliders();
reset();
The red line shows the fraction of Hawks; the dashed line marks the theoretical ESS frequency V/C. Notice that no matter where you start — all Hawks, all Doves, or anything in between — the population converges to the same equilibrium. That convergence is what makes it stable.
Comments
Loading comments...