The landscape below has several food sources with different quality scores. Watch how the colony converges on the richest one.
<!-- ABC demo — food sources with quality scores, three bee roles -->
<p class="hint">Five food sources have different quality scores (shown inside each circle). <b>Employed bees</b> (blue) exploit known sources, <b>onlooker bees</b> (green) prefer richer ones, and <b>scouts</b> (red) explore randomly. The gold ring marks the current best.</p>
<div id="landscape"></div>
<div class="legend">
<span class="dot employed"></span> Employed
<span class="dot onlooker"></span> Onlooker
<span class="dot scout"></span> Scout
</div>
<div class="status" id="status">Press Step or Run to start the colony.</div>
<div class="btns">
<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>
</div>
/* Landscape canvas + bee dots */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#landscape { position: relative; width: 100%; height: 220px; background: #eef2f6;
border-radius: 10px; overflow: hidden; border: 1px solid #d0d8e2; margin-bottom: .4rem; }
.source { position: absolute; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font: 700 11px system-ui; color: #fff;
transition: transform .15s, box-shadow .15s; }
.source.best { box-shadow: 0 0 0 3px #f4c542; }
.bee { position: absolute; width: 10px; height: 10px; border-radius: 50%;
transition: left .3s ease, top .3s ease; }
.bee.employed { background: #2563eb; }
.bee.onlooker { background: #16a34a; }
.bee.scout { background: #dc2626; }
.legend { display: flex; gap: .8rem; font-size: .8rem; color: #555; margin-bottom: .4rem; align-items: center; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.employed { background: #2563eb; }
.dot.onlooker { background: #16a34a; }
.dot.scout { background: #dc2626; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.running { color: #1d4ed8; }
.status.done { color: #15803d; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Artificial Bee Colony demo: employed, onlooker, and scout bees
// find the food source with the highest quality score.
/* Food sources: {id, x%, y%, quality 0-100, display radius} */
const SOURCES = [
{ id: 0, x: 15, y: 30, quality: 30, r: 20 },
{ id: 1, x: 35, y: 60, quality: 65, r: 27 },
{ id: 2, x: 60, y: 25, quality: 85, r: 34 },
{ id: 3, x: 80, y: 65, quality: 50, r: 24 },
{ id: 4, x: 50, y: 75, quality: 40, r: 22 }
];
const NUM_EMPLOYED = 5; // one employed bee per source
const NUM_ONLOOKER = 5; // onlooker bees share exploitation
const NUM_SCOUTS = 2; // scouts inject diversity
const LIMIT = 6; // abandon source after this many visits without gain
/* Colony state: visit counters, best found, cycle number */
let visits = SOURCES.map(() => 0);
let bestId = -1;
let cycle = 0;
let animTimer = null;
let bees = [];
// Fitness = quality / 100 (higher is better)
function fitness(q) { return q / 100; }
// Roulette-wheel selection — sources with higher quality get chosen more often
function rouletteSelect() {
const total = SOURCES.reduce((s, src) => s + fitness(src.quality), 0);
let r = Math.random() * total;
for (const src of SOURCES) { r -= fitness(src.quality); if (r <= 0) return src.id; }
return SOURCES.length - 1;
}
// Find the index of the source with highest quality
function findBest() {
return SOURCES.reduce((b, s) => s.quality > SOURCES[b].quality ? s.id : b, 0);
}
// Draw sources and bees onto the landscape div
function renderLandscape() {
const land = document.getElementById('landscape');
land.innerHTML = '';
const W = land.offsetWidth || 320;
const H = land.offsetHeight || 220;
SOURCES.forEach(src => {
const el = document.createElement('div');
el.className = 'source' + (src.id === bestId ? ' best' : '');
const size = 20 + src.quality * 0.4;
el.style.cssText = [
'width:' + size + 'px', 'height:' + size + 'px',
'left:' + (src.x / 100 * W - size / 2) + 'px',
'top:' + (src.y / 100 * H - size / 2) + 'px',
'background:hsl(' + (80 + src.quality) + ',55%,38%)',
'font-size:' + Math.max(9, size * 0.32) + 'px'
].join(';');
el.textContent = src.quality;
land.appendChild(el);
});
bees.forEach(bee => {
const el = document.createElement('div');
el.className = 'bee ' + bee.role;
const tx = bee.x / 100 * W - 5;
const ty = bee.y / 100 * H - 5;
el.style.left = tx + 'px';
el.style.top = ty + 'px';
land.appendChild(el);
});
}
// Advance the colony by one full cycle
function step() {
cycle++;
bees = [];
/* Employed bees exploit their assigned source with small random jitter */
for (let i = 0; i < NUM_EMPLOYED; i++) {
const src = SOURCES[i % SOURCES.length];
visits[src.id]++;
const jitter = (Math.random() - 0.5) * 14;
bees.push({ role: 'employed', x: Math.max(2, Math.min(98, src.x + jitter)), y: Math.max(2, Math.min(98, src.y + jitter)) });
}
/* Onlooker bees pick a source by roulette and cluster near it */
for (let i = 0; i < NUM_ONLOOKER; i++) {
const sid = rouletteSelect();
const src = SOURCES[sid];
const jitter = (Math.random() - 0.5) * 8;
bees.push({ role: 'onlooker', x: Math.max(2, Math.min(98, src.x + jitter)), y: Math.max(2, Math.min(98, src.y + jitter)) });
}
/* If the most-visited source is exhausted, a scout opens a new random one */
for (let i = 0; i < NUM_SCOUTS; i++) {
let worst = visits.reduce((wId, v, idx) => v > visits[wId] ? idx : wId, 0);
if (visits[worst] >= LIMIT) {
SOURCES[worst].quality = Math.floor(Math.random() * 50) + 10;
visits[worst] = 0;
bees.push({ role: 'scout', x: Math.random() * 96 + 2, y: Math.random() * 96 + 2 });
} else {
bees.push({ role: 'scout', x: Math.random() * 96 + 2, y: Math.random() * 96 + 2 });
}
}
bestId = findBest();
renderLandscape();
const best = SOURCES[bestId];
document.getElementById('status').textContent = i18nFmt('Cycle %cycle% — best quality: %quality%', { cycle: cycle, quality: best.quality });
document.getElementById('status').className = 'status running';
}
function runAuto() {
const btn = document.getElementById('btn-run');
if (animTimer) {
clearInterval(animTimer);
animTimer = null;
btn.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
return;
}
btn.textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
animTimer = setInterval(() => {
if (cycle >= 30) {
clearInterval(animTimer);
animTimer = null;
btn.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = i18nFmt('Converged — best quality found: %quality%', { quality: SOURCES[bestId].quality });
document.getElementById('status').className = 'status done';
return;
}
step();
}, 400);
}
function reset() {
if (animTimer) { clearInterval(animTimer); animTimer = null; }
visits = SOURCES.map(() => 0);
SOURCES[0].quality = 30; SOURCES[1].quality = 65;
SOURCES[2].quality = 85; SOURCES[3].quality = 50; SOURCES[4].quality = 40;
bestId = -1;
cycle = 0;
bees = [];
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = 'Press Step or Run to start the colony.';
document.getElementById('status').className = 'status';
renderLandscape();
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', runAuto);
document.getElementById('btn-reset').addEventListener('click', reset);
renderLandscape();
<!-- ABC demo — food sources with quality scores, three bee roles -->
<p class="hint">Five food sources have different quality scores (shown inside each circle). <b>Employed bees</b> (blue) exploit known sources, <b>onlooker bees</b> (green) prefer richer ones, and <b>scouts</b> (red) explore randomly. The gold ring marks the current best.</p>
<div id="landscape"></div>
<div class="legend">
<span class="dot employed"></span> Employed
<span class="dot onlooker"></span> Onlooker
<span class="dot scout"></span> Scout
</div>
<div class="status" id="status">Press Step or Run to start the colony.</div>
<div class="btns">
<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>
</div>
/* Landscape canvas + bee dots */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#landscape { position: relative; width: 100%; height: 220px; background: #eef2f6;
border-radius: 10px; overflow: hidden; border: 1px solid #d0d8e2; margin-bottom: .4rem; }
.source { position: absolute; border-radius: 50%; display: flex; align-items: center;
justify-content: center; font: 700 11px system-ui; color: #fff;
transition: transform .15s, box-shadow .15s; }
.source.best { box-shadow: 0 0 0 3px #f4c542; }
.bee { position: absolute; width: 10px; height: 10px; border-radius: 50%;
transition: left .3s ease, top .3s ease; }
.bee.employed { background: #2563eb; }
.bee.onlooker { background: #16a34a; }
.bee.scout { background: #dc2626; }
.legend { display: flex; gap: .8rem; font-size: .8rem; color: #555; margin-bottom: .4rem; align-items: center; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.employed { background: #2563eb; }
.dot.onlooker { background: #16a34a; }
.dot.scout { background: #dc2626; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0; }
.status.running { color: #1d4ed8; }
.status.done { color: #15803d; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Artificial Bee Colony demo: employed, onlooker, and scout bees
// find the food source with the highest quality score.
/* Food sources: {id, x%, y%, quality 0-100, display radius} */
const SOURCES = [
{ id: 0, x: 15, y: 30, quality: 30, r: 20 },
{ id: 1, x: 35, y: 60, quality: 65, r: 27 },
{ id: 2, x: 60, y: 25, quality: 85, r: 34 },
{ id: 3, x: 80, y: 65, quality: 50, r: 24 },
{ id: 4, x: 50, y: 75, quality: 40, r: 22 }
];
const NUM_EMPLOYED = 5; // one employed bee per source
const NUM_ONLOOKER = 5; // onlooker bees share exploitation
const NUM_SCOUTS = 2; // scouts inject diversity
const LIMIT = 6; // abandon source after this many visits without gain
/* Colony state: visit counters, best found, cycle number */
let visits = SOURCES.map(() => 0);
let bestId = -1;
let cycle = 0;
let animTimer = null;
let bees = [];
// Fitness = quality / 100 (higher is better)
function fitness(q) { return q / 100; }
// Roulette-wheel selection — sources with higher quality get chosen more often
function rouletteSelect() {
const total = SOURCES.reduce((s, src) => s + fitness(src.quality), 0);
let r = Math.random() * total;
for (const src of SOURCES) { r -= fitness(src.quality); if (r <= 0) return src.id; }
return SOURCES.length - 1;
}
// Find the index of the source with highest quality
function findBest() {
return SOURCES.reduce((b, s) => s.quality > SOURCES[b].quality ? s.id : b, 0);
}
// Draw sources and bees onto the landscape div
function renderLandscape() {
const land = document.getElementById('landscape');
land.innerHTML = '';
const W = land.offsetWidth || 320;
const H = land.offsetHeight || 220;
SOURCES.forEach(src => {
const el = document.createElement('div');
el.className = 'source' + (src.id === bestId ? ' best' : '');
const size = 20 + src.quality * 0.4;
el.style.cssText = [
'width:' + size + 'px', 'height:' + size + 'px',
'left:' + (src.x / 100 * W - size / 2) + 'px',
'top:' + (src.y / 100 * H - size / 2) + 'px',
'background:hsl(' + (80 + src.quality) + ',55%,38%)',
'font-size:' + Math.max(9, size * 0.32) + 'px'
].join(';');
el.textContent = src.quality;
land.appendChild(el);
});
bees.forEach(bee => {
const el = document.createElement('div');
el.className = 'bee ' + bee.role;
const tx = bee.x / 100 * W - 5;
const ty = bee.y / 100 * H - 5;
el.style.left = tx + 'px';
el.style.top = ty + 'px';
land.appendChild(el);
});
}
// Advance the colony by one full cycle
function step() {
cycle++;
bees = [];
/* Employed bees exploit their assigned source with small random jitter */
for (let i = 0; i < NUM_EMPLOYED; i++) {
const src = SOURCES[i % SOURCES.length];
visits[src.id]++;
const jitter = (Math.random() - 0.5) * 14;
bees.push({ role: 'employed', x: Math.max(2, Math.min(98, src.x + jitter)), y: Math.max(2, Math.min(98, src.y + jitter)) });
}
/* Onlooker bees pick a source by roulette and cluster near it */
for (let i = 0; i < NUM_ONLOOKER; i++) {
const sid = rouletteSelect();
const src = SOURCES[sid];
const jitter = (Math.random() - 0.5) * 8;
bees.push({ role: 'onlooker', x: Math.max(2, Math.min(98, src.x + jitter)), y: Math.max(2, Math.min(98, src.y + jitter)) });
}
/* If the most-visited source is exhausted, a scout opens a new random one */
for (let i = 0; i < NUM_SCOUTS; i++) {
let worst = visits.reduce((wId, v, idx) => v > visits[wId] ? idx : wId, 0);
if (visits[worst] >= LIMIT) {
SOURCES[worst].quality = Math.floor(Math.random() * 50) + 10;
visits[worst] = 0;
bees.push({ role: 'scout', x: Math.random() * 96 + 2, y: Math.random() * 96 + 2 });
} else {
bees.push({ role: 'scout', x: Math.random() * 96 + 2, y: Math.random() * 96 + 2 });
}
}
bestId = findBest();
renderLandscape();
const best = SOURCES[bestId];
document.getElementById('status').textContent = i18nFmt('Cycle %cycle% — best quality: %quality%', { cycle: cycle, quality: best.quality });
document.getElementById('status').className = 'status running';
}
function runAuto() {
const btn = document.getElementById('btn-run');
if (animTimer) {
clearInterval(animTimer);
animTimer = null;
btn.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
return;
}
btn.textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
animTimer = setInterval(() => {
if (cycle >= 30) {
clearInterval(animTimer);
animTimer = null;
btn.textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = i18nFmt('Converged — best quality found: %quality%', { quality: SOURCES[bestId].quality });
document.getElementById('status').className = 'status done';
return;
}
step();
}, 400);
}
function reset() {
if (animTimer) { clearInterval(animTimer); animTimer = null; }
visits = SOURCES.map(() => 0);
SOURCES[0].quality = 30; SOURCES[1].quality = 65;
SOURCES[2].quality = 85; SOURCES[3].quality = 50; SOURCES[4].quality = 40;
bestId = -1;
cycle = 0;
bees = [];
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
document.getElementById('status').textContent = 'Press Step or Run to start the colony.';
document.getElementById('status').className = 'status';
renderLandscape();
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-run').addEventListener('click', runAuto);
document.getElementById('btn-reset').addEventListener('click', reset);
renderLandscape();
Comments
Loading comments...