Below, two populations — predators and prey — compete across generations. Each individual has a single numerical trait (its speed). Each generation, predators hunt prey: a predator catches a prey if its speed exceeds the prey's speed by less than a threshold. Survivors reproduce with small random mutations.
<!-- Competitive coevolution demo: predators and prey coevolve each generation -->
<div class="controls">
<button id="btn-run" type="button">Run simulation</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="gen-label">Generation <b id="gen-num">0</b></span>
</div>
<div class="chart-wrap">
<canvas id="chart" width="560" height="200"></canvas>
</div>
<div class="populations">
<div class="pop-box predator">
<div class="pop-title">Predators</div>
<div class="pop-stat">Avg speed: <b id="pred-avg">—</b></div>
<div id="pred-dots" class="dots"></div>
</div>
<div class="pop-box prey">
<div class="pop-title">Prey</div>
<div class="pop-stat">Avg speed: <b id="prey-avg">—</b></div>
<div id="prey-dots" class="dots"></div>
</div>
</div>
<div id="status" class="status"></div>
/* layout styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.gen-label { font-size: .9rem; color: #555; margin-left: auto; }
/* chart area styles */
.chart-wrap { background: #f4f7fa; border-radius: 10px; padding: .4rem; margin-bottom: .6rem; }
canvas { display: block; max-width: 100%; }
/* population box styles */
.populations { display: flex; gap: .6rem; }
.pop-box { flex: 1; border-radius: 10px; padding: .5rem .7rem; }
.pop-box.predator { background: #fff0f0; border: 1px solid #f2b8b8; }
.pop-box.prey { background: #f0f7ff; border: 1px solid #b3d3f5; }
.pop-title { font-weight: 700; font-size: .85rem; margin-bottom: .2rem; }
.predator .pop-title { color: #b91c1c; }
.prey .pop-title { color: #1d4ed8; }
.pop-stat { font-size: .8rem; color: #444; margin-bottom: .35rem; }
/* individual dot styles */
.dots { display: flex; flex-wrap: wrap; gap: 3px; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.predator .dot { background: #e63946; }
.prey .dot { background: #2563eb; }
.status { font-size: .85rem; font-weight: 600; color: #555; min-height: 1.2em; margin-top: .4rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation parameters: population size, mutation spread, catch threshold, max generations */
const POP_SIZE = 20;
const MUTATION_SD = 1.5;
const CATCH_THRESHOLD = 2;
const MAX_GENS = 60;
/* simulation state */
let predators = [], prey = [], generation = 0;
let predHistory = [], preyHistory = [];
let running = false, timer = null;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
/* initialize both populations with random starting speeds around 10 */
function initPop() {
predators = Array.from({ length: POP_SIZE }, () => 10 + Math.random() * 5);
prey = Array.from({ length: POP_SIZE }, () => 10 + Math.random() * 5);
generation = 0;
predHistory = [];
preyHistory = [];
}
/* compute average of an array */
function avg(arr) { return arr.reduce((a, b) => a + b, 0) / arr.length; }
/* mutate a speed value with Gaussian-like noise */
function mutate(v) {
return Math.max(1, v + (Math.random() * 2 - 1) * MUTATION_SD);
}
/* advance one generation */
function step() {
const newPred = [], newPrey = [];
/* each predator attempts to catch a random prey */
const caught = new Set();
predators.forEach((ps, pi) => {
const preyIdx = Math.floor(Math.random() * prey.length);
if (ps > prey[preyIdx] - CATCH_THRESHOLD) caught.add(preyIdx);
});
/* predators that successfully catch prey survive and reproduce */
const successPred = predators.filter((ps, pi) => {
const target = Math.floor(Math.random() * prey.length);
return ps > prey[target] - CATCH_THRESHOLD;
});
const survivedPred = successPred.length ? successPred : predators.slice();
for (let i = 0; i < POP_SIZE; i++) {
const parent = survivedPred[Math.floor(Math.random() * survivedPred.length)];
newPred.push(mutate(parent));
}
/* prey that were not caught survive and reproduce */
const survivedPrey = prey.filter((_, idx) => !caught.has(idx));
const preyPool = survivedPrey.length ? survivedPrey : prey.slice();
for (let i = 0; i < POP_SIZE; i++) {
const parent = preyPool[Math.floor(Math.random() * preyPool.length)];
newPrey.push(mutate(parent));
}
predators = newPred;
prey = newPrey;
generation++;
predHistory.push(avg(predators));
preyHistory.push(avg(prey));
}
/* draw the history chart on the canvas */
function drawChart() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (!predHistory.length) return;
const allVals = [...predHistory, ...preyHistory];
const minV = Math.min(...allVals) - 1, maxV = Math.max(...allVals) + 1;
const xScale = (W - 30) / Math.max(predHistory.length - 1, 1);
const yScale = (H - 20) / (maxV - minV);
const px = (i) => 15 + i * xScale;
const py = (v) => H - 10 - (v - minV) * yScale;
/* draw a line series in the given color */
const drawLine = (hist, color) => {
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
hist.forEach((v, i) => i === 0 ? ctx.moveTo(px(i), py(v)) : ctx.lineTo(px(i), py(v)));
ctx.stroke();
};
drawLine(predHistory, '#e63946');
drawLine(preyHistory, '#2563eb');
}
/* update all UI elements and redraw the chart */
function renderUI() {
document.getElementById('gen-num').textContent = generation;
document.getElementById('pred-avg').textContent = avg(predators).toFixed(1);
document.getElementById('prey-avg').textContent = avg(prey).toFixed(1);
const predDots = document.getElementById('pred-dots');
const preyDots = document.getElementById('prey-dots');
predDots.innerHTML = '';
preyDots.innerHTML = '';
predators.slice(0, 20).forEach(() => { const d = document.createElement('div'); d.className = 'dot'; predDots.appendChild(d); });
prey.slice(0, 20).forEach(() => { const d = document.createElement('div'); d.className = 'dot'; preyDots.appendChild(d); });
drawChart();
const statusEl = document.getElementById('status');
if (generation === 0) {
statusEl.textContent = 'Press Run or Step to start the arms race.';
} else if (generation >= MAX_GENS) {
statusEl.textContent = i18nFmt('Simulation complete after %n% generations.', { n: generation });
} else {
statusEl.textContent = i18nFmt('Generation %n% — the race continues!', { n: generation });
}
}
/* wire up button event listeners */
function setRunning(v) {
running = v;
document.getElementById('btn-run').disabled = v || generation >= MAX_GENS;
document.getElementById('btn-step').disabled = v || generation >= MAX_GENS;
document.getElementById('btn-reset').disabled = v;
}
document.getElementById('btn-run').addEventListener('click', () => {
if (running || generation >= MAX_GENS) return;
setRunning(true);
timer = setInterval(() => {
step();
renderUI();
if (generation >= MAX_GENS) { clearInterval(timer); setRunning(false); }
}, 120);
});
document.getElementById('btn-step').addEventListener('click', () => {
if (running || generation >= MAX_GENS) return;
step();
renderUI();
if (generation >= MAX_GENS) setRunning(false);
});
document.getElementById('btn-reset').addEventListener('click', () => {
clearInterval(timer);
setRunning(false);
initPop();
renderUI();
});
initPop();
renderUI();
<!-- Competitive coevolution demo: predators and prey coevolve each generation -->
<div class="controls">
<button id="btn-run" type="button">Run simulation</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="gen-label">Generation <b id="gen-num">0</b></span>
</div>
<div class="chart-wrap">
<canvas id="chart" width="560" height="200"></canvas>
</div>
<div class="populations">
<div class="pop-box predator">
<div class="pop-title">Predators</div>
<div class="pop-stat">Avg speed: <b id="pred-avg">—</b></div>
<div id="pred-dots" class="dots"></div>
</div>
<div class="pop-box prey">
<div class="pop-title">Prey</div>
<div class="pop-stat">Avg speed: <b id="prey-avg">—</b></div>
<div id="prey-dots" class="dots"></div>
</div>
</div>
<div id="status" class="status"></div>
/* layout styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.gen-label { font-size: .9rem; color: #555; margin-left: auto; }
/* chart area styles */
.chart-wrap { background: #f4f7fa; border-radius: 10px; padding: .4rem; margin-bottom: .6rem; }
canvas { display: block; max-width: 100%; }
/* population box styles */
.populations { display: flex; gap: .6rem; }
.pop-box { flex: 1; border-radius: 10px; padding: .5rem .7rem; }
.pop-box.predator { background: #fff0f0; border: 1px solid #f2b8b8; }
.pop-box.prey { background: #f0f7ff; border: 1px solid #b3d3f5; }
.pop-title { font-weight: 700; font-size: .85rem; margin-bottom: .2rem; }
.predator .pop-title { color: #b91c1c; }
.prey .pop-title { color: #1d4ed8; }
.pop-stat { font-size: .8rem; color: #444; margin-bottom: .35rem; }
/* individual dot styles */
.dots { display: flex; flex-wrap: wrap; gap: 3px; }
.dot { width: 10px; height: 10px; border-radius: 50%; }
.predator .dot { background: #e63946; }
.prey .dot { background: #2563eb; }
.status { font-size: .85rem; font-weight: 600; color: #555; min-height: 1.2em; margin-top: .4rem; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Simulation parameters: population size, mutation spread, catch threshold, max generations */
const POP_SIZE = 20;
const MUTATION_SD = 1.5;
const CATCH_THRESHOLD = 2;
const MAX_GENS = 60;
/* simulation state */
let predators = [], prey = [], generation = 0;
let predHistory = [], preyHistory = [];
let running = false, timer = null;
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
/* initialize both populations with random starting speeds around 10 */
function initPop() {
predators = Array.from({ length: POP_SIZE }, () => 10 + Math.random() * 5);
prey = Array.from({ length: POP_SIZE }, () => 10 + Math.random() * 5);
generation = 0;
predHistory = [];
preyHistory = [];
}
/* compute average of an array */
function avg(arr) { return arr.reduce((a, b) => a + b, 0) / arr.length; }
/* mutate a speed value with Gaussian-like noise */
function mutate(v) {
return Math.max(1, v + (Math.random() * 2 - 1) * MUTATION_SD);
}
/* advance one generation */
function step() {
const newPred = [], newPrey = [];
/* each predator attempts to catch a random prey */
const caught = new Set();
predators.forEach((ps, pi) => {
const preyIdx = Math.floor(Math.random() * prey.length);
if (ps > prey[preyIdx] - CATCH_THRESHOLD) caught.add(preyIdx);
});
/* predators that successfully catch prey survive and reproduce */
const successPred = predators.filter((ps, pi) => {
const target = Math.floor(Math.random() * prey.length);
return ps > prey[target] - CATCH_THRESHOLD;
});
const survivedPred = successPred.length ? successPred : predators.slice();
for (let i = 0; i < POP_SIZE; i++) {
const parent = survivedPred[Math.floor(Math.random() * survivedPred.length)];
newPred.push(mutate(parent));
}
/* prey that were not caught survive and reproduce */
const survivedPrey = prey.filter((_, idx) => !caught.has(idx));
const preyPool = survivedPrey.length ? survivedPrey : prey.slice();
for (let i = 0; i < POP_SIZE; i++) {
const parent = preyPool[Math.floor(Math.random() * preyPool.length)];
newPrey.push(mutate(parent));
}
predators = newPred;
prey = newPrey;
generation++;
predHistory.push(avg(predators));
preyHistory.push(avg(prey));
}
/* draw the history chart on the canvas */
function drawChart() {
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
if (!predHistory.length) return;
const allVals = [...predHistory, ...preyHistory];
const minV = Math.min(...allVals) - 1, maxV = Math.max(...allVals) + 1;
const xScale = (W - 30) / Math.max(predHistory.length - 1, 1);
const yScale = (H - 20) / (maxV - minV);
const px = (i) => 15 + i * xScale;
const py = (v) => H - 10 - (v - minV) * yScale;
/* draw a line series in the given color */
const drawLine = (hist, color) => {
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.beginPath();
hist.forEach((v, i) => i === 0 ? ctx.moveTo(px(i), py(v)) : ctx.lineTo(px(i), py(v)));
ctx.stroke();
};
drawLine(predHistory, '#e63946');
drawLine(preyHistory, '#2563eb');
}
/* update all UI elements and redraw the chart */
function renderUI() {
document.getElementById('gen-num').textContent = generation;
document.getElementById('pred-avg').textContent = avg(predators).toFixed(1);
document.getElementById('prey-avg').textContent = avg(prey).toFixed(1);
const predDots = document.getElementById('pred-dots');
const preyDots = document.getElementById('prey-dots');
predDots.innerHTML = '';
preyDots.innerHTML = '';
predators.slice(0, 20).forEach(() => { const d = document.createElement('div'); d.className = 'dot'; predDots.appendChild(d); });
prey.slice(0, 20).forEach(() => { const d = document.createElement('div'); d.className = 'dot'; preyDots.appendChild(d); });
drawChart();
const statusEl = document.getElementById('status');
if (generation === 0) {
statusEl.textContent = 'Press Run or Step to start the arms race.';
} else if (generation >= MAX_GENS) {
statusEl.textContent = i18nFmt('Simulation complete after %n% generations.', { n: generation });
} else {
statusEl.textContent = i18nFmt('Generation %n% — the race continues!', { n: generation });
}
}
/* wire up button event listeners */
function setRunning(v) {
running = v;
document.getElementById('btn-run').disabled = v || generation >= MAX_GENS;
document.getElementById('btn-step').disabled = v || generation >= MAX_GENS;
document.getElementById('btn-reset').disabled = v;
}
document.getElementById('btn-run').addEventListener('click', () => {
if (running || generation >= MAX_GENS) return;
setRunning(true);
timer = setInterval(() => {
step();
renderUI();
if (generation >= MAX_GENS) { clearInterval(timer); setRunning(false); }
}, 120);
});
document.getElementById('btn-step').addEventListener('click', () => {
if (running || generation >= MAX_GENS) return;
step();
renderUI();
if (generation >= MAX_GENS) setRunning(false);
});
document.getElementById('btn-reset').addEventListener('click', () => {
clearInterval(timer);
setRunning(false);
initPop();
renderUI();
});
initPop();
renderUI();
Watch how both populations' average speeds climb over time. Neither converges to a fixed value — they chase each other in an endless escalation. This is the hallmark of competitive coevolution: the fitness landscape itself keeps shifting because your opponent keeps improving.
Comments
Loading comments...