Each cell in the grid below represents a unique behavioral niche defined by two features: speed (columns) and robustness (rows). A cell's color shows the fitness of the best solution found so far for that niche — darker means higher fitness. Empty cells are niches that haven't been filled yet.
<p class="hint">Each cell is a behavioral niche defined by <b>speed</b> (left→right) and <b>robustness</b> (bottom→top). Color intensity shows fitness — darker = higher. Click <b>Step</b> for one iteration or <b>Run</b> to animate.</p>
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="iter-label">Iterations: <span id="iter-count">0</span></span>
</div>
<div id="map-grid" class="map-grid" aria-label="MAP-Elites behavioral archive grid"></div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
.iter-label { font-size: .85rem; color: #555; margin-left: .2rem; }
.map-grid { display: grid; gap: 3px; margin-bottom: .5rem; }
.cell { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid #c8d3dc;
display: flex; align-items: center; justify-content: center;
font: 600 10px ui-monospace, monospace; color: #fff; transition: background .25s; }
.cell.empty { background: #e8eef3; border-color: #c8d3dc; }
.cell.filled { border-color: transparent; }
.cell.new { outline: 2px solid #f4a261; outline-offset: 1px; }
.row-label, .col-label { font-size: .7rem; color: #666; text-align: center; }
.axis-row { display: flex; align-items: center; gap: 3px; }
.axis-label-y { font-size: .7rem; color: #555; writing-mode: vertical-rl;
transform: rotate(180deg); text-align: center; margin-right: 2px; }
.axis-label-x { font-size: .7rem; color: #555; text-align: center; margin-top: 2px; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// MAP-Elites (Mouret & Clune 2015) — quality-diversity illumination algorithm.
// Grid dimensions
const ROWS = 6, COLS = 8;
// Behavioral dimensions — speed (0=slow, 1=fast) and robustness (0=fragile, 1=robust).
// Fitness function — bimodal landscape with a strong peak near (0.65, 0.35)
// and a secondary bump near (0.15, 0.8), mimicking a rugged search space.
const archive = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
let iterations = 0, runTimer = null, lastCell = null;
function fitness(speed, robustness) {
// Bimodal Gaussian fitness landscape
const cx = 0.65, cy = 0.35;
const dx = speed - cx, dy = robustness - cy;
const base = Math.exp(-(dx * dx + dy * dy) / 0.18);
const bump = 0.35 * Math.exp(-((speed - 0.15) * (speed - 0.15) + (robustness - 0.8) * (robustness - 0.8)) / 0.06);
return Math.min(1, base + bump);
}
function cellCoords(speed, robustness) {
// Map continuous (speed, robustness) to discrete grid cell indices.
const col = Math.min(COLS - 1, Math.floor(speed * COLS));
const row = Math.min(ROWS - 1, Math.floor((1 - robustness) * ROWS));
return { row, col };
}
function fitnessColor(f) {
// Interpolate from light blue (low fitness) to deep blue (high fitness).
const r = Math.round(29 + (224 - 29) * (1 - f));
const g = Math.round(53 + (86 - 53) * (1 - f) + f * 120);
const b = Math.round(87 + (86 - 87) * (1 - f));
const dark = f > 0.5 ? 0 : Math.round(255 * (1 - f * 2));
return `rgb(${Math.round(29 + (52 - 29) * (1 - f))},${Math.round(53 + (168 - 53) * f)},${Math.round(87 + (100 - 87) * (1 - f))})`;
}
function buildGrid() {
const container = document.getElementById('map-grid');
container.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
container.innerHTML = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const d = document.createElement('div');
d.className = 'cell empty';
d.id = `cell-${r}-${c}`;
container.appendChild(d);
}
}
}
function updateCell(row, col, isNew) {
const el = document.getElementById(`cell-${row}-${col}`);
const entry = archive[row][col];
if (!entry) { el.className = 'cell empty'; el.textContent = ''; el.style.background = ''; return; }
el.className = 'cell filled' + (isNew ? ' new' : '');
el.style.background = fitnessColor(entry.fitness);
el.textContent = (entry.fitness * 100).toFixed(0);
if (isNew) setTimeout(() => el.classList.remove('new'), 600);
}
function step() {
// One MAP-Elites iteration — generate or mutate, evaluate, update archive.
let speed, robustness;
const filled = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled.push([r, c]);
if (filled.length === 0 || Math.random() < 0.15) {
// Random initialization (or 15% exploration chance)
speed = Math.random();
robustness = Math.random();
} else {
// Mutate a random elite from the archive
const [er, ec] = filled[Math.floor(Math.random() * filled.length)];
const parent = archive[er][ec];
speed = Math.max(0, Math.min(1, parent.speed + (Math.random() - 0.5) * 0.25));
robustness = Math.max(0, Math.min(1, parent.robustness + (Math.random() - 0.5) * 0.25));
}
const f = fitness(speed, robustness);
const { row, col } = cellCoords(speed, robustness);
const prev = archive[row][col];
iterations++;
document.getElementById('iter-count').textContent = iterations;
if (!prev || f > prev.fitness) {
archive[row][col] = { speed, robustness, fitness: f };
lastCell = { row, col };
updateCell(row, col, true);
const filled2 = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled2.push([r, c]);
const qdScore = filled2.reduce((s, [r, c]) => s + archive[r][c].fitness, 0);
document.getElementById('status').textContent =
i18nFmt('%filled% / %total% niches filled · QD-score: %qd%', { filled: filled2.length, total: ROWS * COLS, qd: qdScore.toFixed(1) });
}
}
function reset() {
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) archive[r][c] = null;
iterations = 0;
document.getElementById('iter-count').textContent = '0';
document.getElementById('status').textContent = '';
buildGrid();
}
let running = false;
function toggleRun() {
running = !running;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
document.getElementById('btn-step').disabled = running;
if (running) runLoop();
}
function runLoop() {
if (!running) return;
step();
const filled = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled.push(1);
if (filled.length >= ROWS * COLS) {
running = false;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = 'Map fully illuminated!';
return;
}
setTimeout(runLoop, 60);
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', toggleRun);
document.getElementById('btn-reset').addEventListener('click', reset);
buildGrid();
<p class="hint">Each cell is a behavioral niche defined by <b>speed</b> (left→right) and <b>robustness</b> (bottom→top). Color intensity shows fitness — darker = higher. Click <b>Step</b> for one iteration or <b>Run</b> to animate.</p>
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="iter-label">Iterations: <span id="iter-count">0</span></span>
</div>
<div id="map-grid" class="map-grid" aria-label="MAP-Elites behavioral archive grid"></div>
<div id="status" class="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui, sans-serif; padding: .38rem .8rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: not-allowed; }
.iter-label { font-size: .85rem; color: #555; margin-left: .2rem; }
.map-grid { display: grid; gap: 3px; margin-bottom: .5rem; }
.cell { width: 100%; aspect-ratio: 1; border-radius: 5px; border: 1px solid #c8d3dc;
display: flex; align-items: center; justify-content: center;
font: 600 10px ui-monospace, monospace; color: #fff; transition: background .25s; }
.cell.empty { background: #e8eef3; border-color: #c8d3dc; }
.cell.filled { border-color: transparent; }
.cell.new { outline: 2px solid #f4a261; outline-offset: 1px; }
.row-label, .col-label { font-size: .7rem; color: #666; text-align: center; }
.axis-row { display: flex; align-items: center; gap: 3px; }
.axis-label-y { font-size: .7rem; color: #555; writing-mode: vertical-rl;
transform: rotate(180deg); text-align: center; margin-right: 2px; }
.axis-label-x { font-size: .7rem; color: #555; text-align: center; margin-top: 2px; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// MAP-Elites (Mouret & Clune 2015) — quality-diversity illumination algorithm.
// Grid dimensions
const ROWS = 6, COLS = 8;
// Behavioral dimensions — speed (0=slow, 1=fast) and robustness (0=fragile, 1=robust).
// Fitness function — bimodal landscape with a strong peak near (0.65, 0.35)
// and a secondary bump near (0.15, 0.8), mimicking a rugged search space.
const archive = Array.from({ length: ROWS }, () => Array(COLS).fill(null));
let iterations = 0, runTimer = null, lastCell = null;
function fitness(speed, robustness) {
// Bimodal Gaussian fitness landscape
const cx = 0.65, cy = 0.35;
const dx = speed - cx, dy = robustness - cy;
const base = Math.exp(-(dx * dx + dy * dy) / 0.18);
const bump = 0.35 * Math.exp(-((speed - 0.15) * (speed - 0.15) + (robustness - 0.8) * (robustness - 0.8)) / 0.06);
return Math.min(1, base + bump);
}
function cellCoords(speed, robustness) {
// Map continuous (speed, robustness) to discrete grid cell indices.
const col = Math.min(COLS - 1, Math.floor(speed * COLS));
const row = Math.min(ROWS - 1, Math.floor((1 - robustness) * ROWS));
return { row, col };
}
function fitnessColor(f) {
// Interpolate from light blue (low fitness) to deep blue (high fitness).
const r = Math.round(29 + (224 - 29) * (1 - f));
const g = Math.round(53 + (86 - 53) * (1 - f) + f * 120);
const b = Math.round(87 + (86 - 87) * (1 - f));
const dark = f > 0.5 ? 0 : Math.round(255 * (1 - f * 2));
return `rgb(${Math.round(29 + (52 - 29) * (1 - f))},${Math.round(53 + (168 - 53) * f)},${Math.round(87 + (100 - 87) * (1 - f))})`;
}
function buildGrid() {
const container = document.getElementById('map-grid');
container.style.gridTemplateColumns = `repeat(${COLS}, 1fr)`;
container.innerHTML = '';
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const d = document.createElement('div');
d.className = 'cell empty';
d.id = `cell-${r}-${c}`;
container.appendChild(d);
}
}
}
function updateCell(row, col, isNew) {
const el = document.getElementById(`cell-${row}-${col}`);
const entry = archive[row][col];
if (!entry) { el.className = 'cell empty'; el.textContent = ''; el.style.background = ''; return; }
el.className = 'cell filled' + (isNew ? ' new' : '');
el.style.background = fitnessColor(entry.fitness);
el.textContent = (entry.fitness * 100).toFixed(0);
if (isNew) setTimeout(() => el.classList.remove('new'), 600);
}
function step() {
// One MAP-Elites iteration — generate or mutate, evaluate, update archive.
let speed, robustness;
const filled = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled.push([r, c]);
if (filled.length === 0 || Math.random() < 0.15) {
// Random initialization (or 15% exploration chance)
speed = Math.random();
robustness = Math.random();
} else {
// Mutate a random elite from the archive
const [er, ec] = filled[Math.floor(Math.random() * filled.length)];
const parent = archive[er][ec];
speed = Math.max(0, Math.min(1, parent.speed + (Math.random() - 0.5) * 0.25));
robustness = Math.max(0, Math.min(1, parent.robustness + (Math.random() - 0.5) * 0.25));
}
const f = fitness(speed, robustness);
const { row, col } = cellCoords(speed, robustness);
const prev = archive[row][col];
iterations++;
document.getElementById('iter-count').textContent = iterations;
if (!prev || f > prev.fitness) {
archive[row][col] = { speed, robustness, fitness: f };
lastCell = { row, col };
updateCell(row, col, true);
const filled2 = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled2.push([r, c]);
const qdScore = filled2.reduce((s, [r, c]) => s + archive[r][c].fitness, 0);
document.getElementById('status').textContent =
i18nFmt('%filled% / %total% niches filled · QD-score: %qd%', { filled: filled2.length, total: ROWS * COLS, qd: qdScore.toFixed(1) });
}
}
function reset() {
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) archive[r][c] = null;
iterations = 0;
document.getElementById('iter-count').textContent = '0';
document.getElementById('status').textContent = '';
buildGrid();
}
let running = false;
function toggleRun() {
running = !running;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
document.getElementById('btn-step').disabled = running;
if (running) runLoop();
}
function runLoop() {
if (!running) return;
step();
const filled = [];
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) if (archive[r][c]) filled.push(1);
if (filled.length >= ROWS * COLS) {
running = false;
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = 'Map fully illuminated!';
return;
}
setTimeout(runLoop, 60);
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', toggleRun);
document.getElementById('btn-reset').addEventListener('click', reset);
buildGrid();
Comments
Loading comments...