The demo below runs a classic phrase-evolution experiment. The target is a short string. A population of random strings evolves toward it one generation at a time through selection, crossover, and mutation.
<div class="ga-wrap">
<div class="target-row">
<label>Target phrase:</label>
<input id="target" type="text" value="HELLO WORLD" maxlength="20" spellcheck="false"/>
</div>
<div class="params-row">
<label>Pop size: <input id="popsize" type="number" min="10" max="200" value="60"/></label>
<label>Mutation rate: <input id="mutrate" type="number" min="1" max="50" value="4"/>%</label>
</div>
<div class="btns">
<button id="btnStart" type="button">▶ Start</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="stats">
<span>Generation: <b id="genNum">0</b></span>
<span>Best fitness: <b id="bestFit">—</b></span>
</div>
<div class="best-label">Best individual:</div>
<div id="bestStr" class="best-str">—</div>
<canvas id="chart" width="480" height="120"></canvas>
<div id="statusMsg" class="status"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.ga-wrap { max-width: 480px; }
.target-row, .params-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .45rem; font-size: .88rem; }
.target-row label, .params-row label { color: #555; }
input[type="text"] { font: 600 1rem ui-monospace, monospace; padding: .28rem .5rem; border: 1px solid #bbb; border-radius: 6px; width: 12rem; text-transform: uppercase; }
input[type="number"] { width: 4.5rem; padding: .28rem .4rem; border: 1px solid #bbb; border-radius: 6px; font-size: .88rem; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .88rem; color: #444; display: flex; gap: 1.2rem; margin-bottom: .3rem; }
.stats b { color: #1d3557; }
.best-label { font-size: .8rem; color: #777; margin-bottom: .15rem; }
.best-str { font: 700 1.25rem ui-monospace, monospace; letter-spacing: .08em; min-height: 1.8rem; margin-bottom: .5rem; }
.best-str .match { color: #0a7d33; }
.best-str .miss { color: #c92f3c; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; margin-bottom: .4rem; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Genetic algorithm that evolves a population of strings toward a target phrase.
// Fitness = number of characters that match the target at the correct position.
// Selection: tournament (k=3). Crossover: single-point. Mutation: random char.
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
function getTarget() {
return document.getElementById('target').value.toUpperCase().replace(/[^A-Z ]/g,'').slice(0,20) || 'HELLO WORLD';
}
function getPopSize() { return Math.max(10, Math.min(200, +document.getElementById('popsize').value || 60)); }
function getMutRate() { return Math.max(1, Math.min(50, +document.getElementById('mutrate').value || 4)) / 100; }
function randChar() { return CHARS[Math.floor(Math.random() * CHARS.length)]; }
function randIndividual(len) { return Array.from({length: len}, randChar).join(''); }
function fitness(ind, target) {
let score = 0;
for (let i = 0; i < target.length; i++) if (ind[i] === target[i]) score++;
return score;
}
function tournament(pop, fitnesses, k) {
let best = Math.floor(Math.random() * pop.length);
for (let i = 1; i < k; i++) {
const c = Math.floor(Math.random() * pop.length);
if (fitnesses[c] > fitnesses[best]) best = c;
}
return pop[best];
}
function crossover(a, b) {
const pt = 1 + Math.floor(Math.random() * (a.length - 1));
return a.slice(0, pt) + b.slice(pt);
}
function mutate(ind, rate) {
return ind.split('').map(c => Math.random() < rate ? randChar() : c).join('');
}
let state = null;
let running = false;
let rafId = null;
const history = []; // best fitness per generation
const MAX_HIST = 200; // cap chart points
function initState() {
const target = getTarget();
const popSize = getPopSize();
const pop = Array.from({length: popSize}, () => randIndividual(target.length));
state = { target, pop, gen: 0 };
history.length = 0;
}
function step() {
if (!state) initState();
const { target, pop } = state;
const fits = pop.map(ind => fitness(ind, target));
const maxFit = Math.max(...fits);
history.push(maxFit);
if (history.length > MAX_HIST) history.shift();
const bestIdx = fits.indexOf(maxFit);
state.gen++;
updateUI(pop[bestIdx], maxFit, target);
drawChart(target.length);
if (maxFit === target.length) {
document.getElementById('statusMsg').textContent =
fmt('✓ Target reached in generation %gen%!', { gen: state.gen });
stopRunning();
return true; // done
}
// Breed next generation
const mutRate = getMutRate();
const newPop = [pop[bestIdx]]; // elitism: keep best
while (newPop.length < pop.length) {
const p1 = tournament(pop, fits, 3);
const p2 = tournament(pop, fits, 3);
newPop.push(mutate(crossover(p1, p2), mutRate));
}
state.pop = newPop;
return false;
}
function updateUI(best, maxFit, target) {
document.getElementById('genNum').textContent = state.gen;
document.getElementById('bestFit').textContent = maxFit + ' / ' + target.length;
const spans = best.split('').map((c, i) =>
`<span class="${c === target[i] ? 'match' : 'miss'}">${c}</span>`
).join('');
document.getElementById('bestStr').innerHTML = spans;
}
function drawChart(targetLen) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// grid lines
ctx.strokeStyle = '#dde3ea'; ctx.lineWidth = 1;
for (let f = 0; f <= targetLen; f += Math.ceil(targetLen / 4)) {
const y = H - 8 - (f / targetLen) * (H - 16);
ctx.beginPath(); ctx.moveTo(30, y); ctx.lineTo(W - 4, y); ctx.stroke();
ctx.fillStyle = '#aaa'; ctx.font = '10px system-ui';
ctx.fillText(f, 2, y + 4);
}
if (history.length < 2) return;
ctx.strokeStyle = '#1d6fa5'; ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((v, i) => {
const x = 30 + (i / (MAX_HIST - 1)) * (W - 34);
const y = H - 8 - (v / targetLen) * (H - 16);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
function startRunning() {
if (running) return;
running = true;
document.getElementById('btnStart').textContent = '⏸ Pause';
function loop() {
if (!running) return;
const done = step();
if (!done) rafId = requestAnimationFrame(loop);
}
loop();
}
function stopRunning() {
running = false;
document.getElementById('btnStart').textContent = '▶ Start';
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
}
document.getElementById('btnStart').addEventListener('click', () => {
if (running) stopRunning(); else startRunning();
});
document.getElementById('btnStep').addEventListener('click', () => {
stopRunning();
if (!state) initState();
document.getElementById('statusMsg').textContent = '';
step();
});
document.getElementById('btnReset').addEventListener('click', () => {
stopRunning();
initState();
document.getElementById('genNum').textContent = '0';
document.getElementById('bestFit').textContent = '—';
document.getElementById('bestStr').textContent = '—';
document.getElementById('statusMsg').textContent = '';
drawChart(getTarget().length);
});
document.getElementById('target').addEventListener('input', () => {
stopRunning();
state = null;
document.getElementById('genNum').textContent = '0';
document.getElementById('bestFit').textContent = '—';
document.getElementById('bestStr').textContent = '—';
document.getElementById('statusMsg').textContent = '';
const canvas = document.getElementById('chart');
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
});
<div class="ga-wrap">
<div class="target-row">
<label>Target phrase:</label>
<input id="target" type="text" value="HELLO WORLD" maxlength="20" spellcheck="false"/>
</div>
<div class="params-row">
<label>Pop size: <input id="popsize" type="number" min="10" max="200" value="60"/></label>
<label>Mutation rate: <input id="mutrate" type="number" min="1" max="50" value="4"/>%</label>
</div>
<div class="btns">
<button id="btnStart" type="button">▶ Start</button>
<button id="btnStep" type="button">Step</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
<div class="stats">
<span>Generation: <b id="genNum">0</b></span>
<span>Best fitness: <b id="bestFit">—</b></span>
</div>
<div class="best-label">Best individual:</div>
<div id="bestStr" class="best-str">—</div>
<canvas id="chart" width="480" height="120"></canvas>
<div id="statusMsg" class="status"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.ga-wrap { max-width: 480px; }
.target-row, .params-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .45rem; font-size: .88rem; }
.target-row label, .params-row label { color: #555; }
input[type="text"] { font: 600 1rem ui-monospace, monospace; padding: .28rem .5rem; border: 1px solid #bbb; border-radius: 6px; width: 12rem; text-transform: uppercase; }
input[type="number"] { width: 4.5rem; padding: .28rem .4rem; border: 1px solid #bbb; border-radius: 6px; font-size: .88rem; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin: .3rem 0 .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.stats { font-size: .88rem; color: #444; display: flex; gap: 1.2rem; margin-bottom: .3rem; }
.stats b { color: #1d3557; }
.best-label { font-size: .8rem; color: #777; margin-bottom: .15rem; }
.best-str { font: 700 1.25rem ui-monospace, monospace; letter-spacing: .08em; min-height: 1.8rem; margin-bottom: .5rem; }
.best-str .match { color: #0a7d33; }
.best-str .miss { color: #c92f3c; }
canvas { display: block; border: 1px solid #dde3ea; border-radius: 8px; background: #f7f9fb; margin-bottom: .4rem; }
.status { font-size: .9rem; font-weight: 600; min-height: 1.4em; color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Genetic algorithm that evolves a population of strings toward a target phrase.
// Fitness = number of characters that match the target at the correct position.
// Selection: tournament (k=3). Crossover: single-point. Mutation: random char.
const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
function getTarget() {
return document.getElementById('target').value.toUpperCase().replace(/[^A-Z ]/g,'').slice(0,20) || 'HELLO WORLD';
}
function getPopSize() { return Math.max(10, Math.min(200, +document.getElementById('popsize').value || 60)); }
function getMutRate() { return Math.max(1, Math.min(50, +document.getElementById('mutrate').value || 4)) / 100; }
function randChar() { return CHARS[Math.floor(Math.random() * CHARS.length)]; }
function randIndividual(len) { return Array.from({length: len}, randChar).join(''); }
function fitness(ind, target) {
let score = 0;
for (let i = 0; i < target.length; i++) if (ind[i] === target[i]) score++;
return score;
}
function tournament(pop, fitnesses, k) {
let best = Math.floor(Math.random() * pop.length);
for (let i = 1; i < k; i++) {
const c = Math.floor(Math.random() * pop.length);
if (fitnesses[c] > fitnesses[best]) best = c;
}
return pop[best];
}
function crossover(a, b) {
const pt = 1 + Math.floor(Math.random() * (a.length - 1));
return a.slice(0, pt) + b.slice(pt);
}
function mutate(ind, rate) {
return ind.split('').map(c => Math.random() < rate ? randChar() : c).join('');
}
let state = null;
let running = false;
let rafId = null;
const history = []; // best fitness per generation
const MAX_HIST = 200; // cap chart points
function initState() {
const target = getTarget();
const popSize = getPopSize();
const pop = Array.from({length: popSize}, () => randIndividual(target.length));
state = { target, pop, gen: 0 };
history.length = 0;
}
function step() {
if (!state) initState();
const { target, pop } = state;
const fits = pop.map(ind => fitness(ind, target));
const maxFit = Math.max(...fits);
history.push(maxFit);
if (history.length > MAX_HIST) history.shift();
const bestIdx = fits.indexOf(maxFit);
state.gen++;
updateUI(pop[bestIdx], maxFit, target);
drawChart(target.length);
if (maxFit === target.length) {
document.getElementById('statusMsg').textContent =
fmt('✓ Target reached in generation %gen%!', { gen: state.gen });
stopRunning();
return true; // done
}
// Breed next generation
const mutRate = getMutRate();
const newPop = [pop[bestIdx]]; // elitism: keep best
while (newPop.length < pop.length) {
const p1 = tournament(pop, fits, 3);
const p2 = tournament(pop, fits, 3);
newPop.push(mutate(crossover(p1, p2), mutRate));
}
state.pop = newPop;
return false;
}
function updateUI(best, maxFit, target) {
document.getElementById('genNum').textContent = state.gen;
document.getElementById('bestFit').textContent = maxFit + ' / ' + target.length;
const spans = best.split('').map((c, i) =>
`<span class="${c === target[i] ? 'match' : 'miss'}">${c}</span>`
).join('');
document.getElementById('bestStr').innerHTML = spans;
}
function drawChart(targetLen) {
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// grid lines
ctx.strokeStyle = '#dde3ea'; ctx.lineWidth = 1;
for (let f = 0; f <= targetLen; f += Math.ceil(targetLen / 4)) {
const y = H - 8 - (f / targetLen) * (H - 16);
ctx.beginPath(); ctx.moveTo(30, y); ctx.lineTo(W - 4, y); ctx.stroke();
ctx.fillStyle = '#aaa'; ctx.font = '10px system-ui';
ctx.fillText(f, 2, y + 4);
}
if (history.length < 2) return;
ctx.strokeStyle = '#1d6fa5'; ctx.lineWidth = 2;
ctx.beginPath();
history.forEach((v, i) => {
const x = 30 + (i / (MAX_HIST - 1)) * (W - 34);
const y = H - 8 - (v / targetLen) * (H - 16);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.stroke();
}
function startRunning() {
if (running) return;
running = true;
document.getElementById('btnStart').textContent = '⏸ Pause';
function loop() {
if (!running) return;
const done = step();
if (!done) rafId = requestAnimationFrame(loop);
}
loop();
}
function stopRunning() {
running = false;
document.getElementById('btnStart').textContent = '▶ Start';
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
}
document.getElementById('btnStart').addEventListener('click', () => {
if (running) stopRunning(); else startRunning();
});
document.getElementById('btnStep').addEventListener('click', () => {
stopRunning();
if (!state) initState();
document.getElementById('statusMsg').textContent = '';
step();
});
document.getElementById('btnReset').addEventListener('click', () => {
stopRunning();
initState();
document.getElementById('genNum').textContent = '0';
document.getElementById('bestFit').textContent = '—';
document.getElementById('bestStr').textContent = '—';
document.getElementById('statusMsg').textContent = '';
drawChart(getTarget().length);
});
document.getElementById('target').addEventListener('input', () => {
stopRunning();
state = null;
document.getElementById('genNum').textContent = '0';
document.getElementById('bestFit').textContent = '—';
document.getElementById('bestStr').textContent = '—';
document.getElementById('statusMsg').textContent = '';
const canvas = document.getElementById('chart');
canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
});
Notice how the population improves fast at first, then slows as the remaining wrong characters become harder to fix. The algorithm has no idea what the target looks like — it only knows the fitness score. This is the same dynamic that plays out in every real GA: rapid early improvement, then a race against diminishing returns and local optima.
Comments
Loading comments...