Below is a one-dimensional landscape. The dot is the current solution; the blue fill is the rising water. At each step the algorithm tries a random neighbor and accepts it only if it stays above the waterline. Watch as the flood closes off lower valleys and forces the dot upward.
<!-- Main layout — controls bar + canvas + status line -->
<div class="controls">
<label>Rain Rate <span id="rain-val">3</span>%
<input type="range" id="rain-slider" min="1" max="10" value="3" step="1">
</label>
<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>
<canvas id="cv" width="560" height="230"></canvas>
<div class="status" id="status">Press Run to start, or Step one move at a time.</div>
/* Minimal reset + layout styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.controls { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; white-space: nowrap; }
input[type=range] { width: 80px; accent-color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .8rem; 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; }
canvas { display: block; width: 100%; max-width: 560px; border-radius: 8px;
background: #f4f7fa; border: 1px solid #cdd9e3; }
/* Status line color coding */
.status { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.running { color: #1d63b0; }
.status.done { color: #0a7d33; }
.status.stuck { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Landscape function — sum of sinusoids to create multiple peaks and valleys
const W = 560, H = 230, PAD = 30;
const PLOT_W = W - PAD * 2, PLOT_H = H - PAD * 2;
// Multiple sinusoids produce a bumpy landscape with a clear global peak
function landscape(x) {
return 0.55 * Math.sin(x * 1.8) +
0.25 * Math.sin(x * 4.5 + 1.2) +
0.12 * Math.sin(x * 9.0 + 0.7) +
0.08 * Math.sin(x * 17.0 + 2.1);
}
// Sample the landscape at N evenly spaced positions
const N = 200;
const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
const ys = xs.map(landscape);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
const yRange = yMax - yMin;
function norm(y) { return (y - yMin) / yRange; } // Map y to [0,1] for water-level comparisons
// Algorithm state
let pos = 0; // current index into xs/ys
let waterLevel = 0; // normalized water level in [0,1]
let running = false;
let animId = null;
let stepCount = 0;
const MAX_STEPS = 600;
const NEIGHBOR_RADIUS = 12; // max index distance for a single step
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const rainSlider = document.getElementById('rain-slider');
const rainVal = document.getElementById('rain-val');
const btnStep = document.getElementById('btn-step');
const btnRun = document.getElementById('btn-run');
const btnReset = document.getElementById('btn-reset');
// Map logical coordinates to canvas pixels
function toCanvasX(i) { return PAD + (i / (N - 1)) * PLOT_W; }
function toCanvasY(y) { return PAD + PLOT_H - norm(y) * PLOT_H; }
function draw() {
ctx.clearRect(0, 0, W, H);
// Blue water fill below the water level
const wlY = toCanvasY(waterLevel * yRange + yMin);
ctx.fillStyle = 'rgba(100,160,230,0.25)';
ctx.fillRect(PAD, wlY, PLOT_W, H - PAD - wlY + 2);
// Draw the landscape curve
ctx.beginPath();
ctx.moveTo(toCanvasX(0), toCanvasY(ys[0]));
for (let i = 1; i < N; i++) ctx.lineTo(toCanvasX(i), toCanvasY(ys[i]));
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
// Dashed waterline
ctx.beginPath();
ctx.moveTo(PAD, wlY);
ctx.lineTo(PAD + PLOT_W, wlY);
ctx.strokeStyle = 'rgba(50,130,220,0.8)';
ctx.lineWidth = 1.5;
ctx.setLineDash([6, 4]);
ctx.stroke();
ctx.setLineDash([]);
// Red dot for current solution position
const dx = toCanvasX(pos);
const dy = toCanvasY(ys[pos]);
ctx.beginPath();
ctx.arc(dx, dy, 7, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function getRainRate() {
return Number(rainSlider.value) * 0.001 * 0.5;
}
// One step of the Great Deluge algorithm
function step() {
const lo = Math.max(0, pos - NEIGHBOR_RADIUS);
const hi = Math.min(N - 1, pos + NEIGHBOR_RADIUS);
const candidate = lo + Math.floor(Math.random() * (hi - lo + 1));
const candidateNorm = norm(ys[candidate]);
// Accept the move only if the candidate is at or above the current water level
if (candidateNorm >= waterLevel) {
pos = candidate;
}
// Raise the water level by the rain rate after each step
waterLevel = Math.min(waterLevel + getRainRate(), 1);
stepCount++;
}
function setStatus(msg, cls) {
statusEl.textContent = msg;
statusEl.className = 'status ' + cls;
}
function reset() {
cancelAnimationFrame(animId);
running = false;
pos = Math.floor(Math.random() * N);
waterLevel = norm(ys[pos]) * 0.05;
stepCount = 0;
btnRun.textContent = 'Run';
btnStep.disabled = false;
draw();
setStatus('Press Run to start, or Step one move at a time.', '');
}
function runLoop() {
if (!running) return;
const quality = norm(ys[pos]);
for (let i = 0; i < 3; i++) step(); // Run 3 steps per animation frame for visible motion
draw();
if (stepCount >= MAX_STEPS || waterLevel >= 0.98) {
running = false;
btnRun.textContent = 'Run';
btnStep.disabled = false;
const pct = Math.round(norm(ys[pos]) * 100);
setStatus(i18nFmt('Settled at quality %pct%% — flood complete.', { pct: pct }), 'done');
return;
}
setStatus(i18nFmt('Step %step% — water at %wl%%', { step: stepCount, wl: Math.round(waterLevel * 100) }), 'running');
animId = requestAnimationFrame(runLoop);
}
btnStep.addEventListener('click', () => {
if (running) return;
step();
draw();
const pct = Math.round(norm(ys[pos]) * 100);
setStatus(i18nFmt('Step %step% — quality %pct%%, water at %wl%%', { step: stepCount, pct: pct, wl: Math.round(waterLevel * 100) }), '');
});
btnRun.addEventListener('click', () => {
if (running) {
running = false;
cancelAnimationFrame(animId);
btnRun.textContent = 'Run';
btnStep.disabled = false;
setStatus('Paused. Press Run to continue.', '');
} else {
running = true;
btnRun.textContent = 'Pause';
btnStep.disabled = true;
runLoop();
}
});
btnReset.addEventListener('click', reset);
rainSlider.addEventListener('input', () => { rainVal.textContent = rainSlider.value; });
reset();
<!-- Main layout — controls bar + canvas + status line -->
<div class="controls">
<label>Rain Rate <span id="rain-val">3</span>%
<input type="range" id="rain-slider" min="1" max="10" value="3" step="1">
</label>
<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>
<canvas id="cv" width="560" height="230"></canvas>
<div class="status" id="status">Press Run to start, or Step one move at a time.</div>
/* Minimal reset + layout styles */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; background: transparent; }
.controls { display: flex; gap: .6rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .85rem; display: flex; align-items: center; gap: .4rem; white-space: nowrap; }
input[type=range] { width: 80px; accent-color: #1d3557; }
button { font: 600 13px system-ui; padding: .4rem .8rem; 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; }
canvas { display: block; width: 100%; max-width: 560px; border-radius: 8px;
background: #f4f7fa; border: 1px solid #cdd9e3; }
/* Status line color coding */
.status { font-size: .9rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; }
.status.running { color: #1d63b0; }
.status.done { color: #0a7d33; }
.status.stuck { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Landscape function — sum of sinusoids to create multiple peaks and valleys
const W = 560, H = 230, PAD = 30;
const PLOT_W = W - PAD * 2, PLOT_H = H - PAD * 2;
// Multiple sinusoids produce a bumpy landscape with a clear global peak
function landscape(x) {
return 0.55 * Math.sin(x * 1.8) +
0.25 * Math.sin(x * 4.5 + 1.2) +
0.12 * Math.sin(x * 9.0 + 0.7) +
0.08 * Math.sin(x * 17.0 + 2.1);
}
// Sample the landscape at N evenly spaced positions
const N = 200;
const xs = Array.from({ length: N }, (_, i) => i / (N - 1));
const ys = xs.map(landscape);
const yMin = Math.min(...ys);
const yMax = Math.max(...ys);
const yRange = yMax - yMin;
function norm(y) { return (y - yMin) / yRange; } // Map y to [0,1] for water-level comparisons
// Algorithm state
let pos = 0; // current index into xs/ys
let waterLevel = 0; // normalized water level in [0,1]
let running = false;
let animId = null;
let stepCount = 0;
const MAX_STEPS = 600;
const NEIGHBOR_RADIUS = 12; // max index distance for a single step
const cv = document.getElementById('cv');
const ctx = cv.getContext('2d');
const statusEl = document.getElementById('status');
const rainSlider = document.getElementById('rain-slider');
const rainVal = document.getElementById('rain-val');
const btnStep = document.getElementById('btn-step');
const btnRun = document.getElementById('btn-run');
const btnReset = document.getElementById('btn-reset');
// Map logical coordinates to canvas pixels
function toCanvasX(i) { return PAD + (i / (N - 1)) * PLOT_W; }
function toCanvasY(y) { return PAD + PLOT_H - norm(y) * PLOT_H; }
function draw() {
ctx.clearRect(0, 0, W, H);
// Blue water fill below the water level
const wlY = toCanvasY(waterLevel * yRange + yMin);
ctx.fillStyle = 'rgba(100,160,230,0.25)';
ctx.fillRect(PAD, wlY, PLOT_W, H - PAD - wlY + 2);
// Draw the landscape curve
ctx.beginPath();
ctx.moveTo(toCanvasX(0), toCanvasY(ys[0]));
for (let i = 1; i < N; i++) ctx.lineTo(toCanvasX(i), toCanvasY(ys[i]));
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
// Dashed waterline
ctx.beginPath();
ctx.moveTo(PAD, wlY);
ctx.lineTo(PAD + PLOT_W, wlY);
ctx.strokeStyle = 'rgba(50,130,220,0.8)';
ctx.lineWidth = 1.5;
ctx.setLineDash([6, 4]);
ctx.stroke();
ctx.setLineDash([]);
// Red dot for current solution position
const dx = toCanvasX(pos);
const dy = toCanvasY(ys[pos]);
ctx.beginPath();
ctx.arc(dx, dy, 7, 0, Math.PI * 2);
ctx.fillStyle = '#e63946';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
function getRainRate() {
return Number(rainSlider.value) * 0.001 * 0.5;
}
// One step of the Great Deluge algorithm
function step() {
const lo = Math.max(0, pos - NEIGHBOR_RADIUS);
const hi = Math.min(N - 1, pos + NEIGHBOR_RADIUS);
const candidate = lo + Math.floor(Math.random() * (hi - lo + 1));
const candidateNorm = norm(ys[candidate]);
// Accept the move only if the candidate is at or above the current water level
if (candidateNorm >= waterLevel) {
pos = candidate;
}
// Raise the water level by the rain rate after each step
waterLevel = Math.min(waterLevel + getRainRate(), 1);
stepCount++;
}
function setStatus(msg, cls) {
statusEl.textContent = msg;
statusEl.className = 'status ' + cls;
}
function reset() {
cancelAnimationFrame(animId);
running = false;
pos = Math.floor(Math.random() * N);
waterLevel = norm(ys[pos]) * 0.05;
stepCount = 0;
btnRun.textContent = 'Run';
btnStep.disabled = false;
draw();
setStatus('Press Run to start, or Step one move at a time.', '');
}
function runLoop() {
if (!running) return;
const quality = norm(ys[pos]);
for (let i = 0; i < 3; i++) step(); // Run 3 steps per animation frame for visible motion
draw();
if (stepCount >= MAX_STEPS || waterLevel >= 0.98) {
running = false;
btnRun.textContent = 'Run';
btnStep.disabled = false;
const pct = Math.round(norm(ys[pos]) * 100);
setStatus(i18nFmt('Settled at quality %pct%% — flood complete.', { pct: pct }), 'done');
return;
}
setStatus(i18nFmt('Step %step% — water at %wl%%', { step: stepCount, wl: Math.round(waterLevel * 100) }), 'running');
animId = requestAnimationFrame(runLoop);
}
btnStep.addEventListener('click', () => {
if (running) return;
step();
draw();
const pct = Math.round(norm(ys[pos]) * 100);
setStatus(i18nFmt('Step %step% — quality %pct%%, water at %wl%%', { step: stepCount, pct: pct, wl: Math.round(waterLevel * 100) }), '');
});
btnRun.addEventListener('click', () => {
if (running) {
running = false;
cancelAnimationFrame(animId);
btnRun.textContent = 'Run';
btnStep.disabled = false;
setStatus('Paused. Press Run to continue.', '');
} else {
running = true;
btnRun.textContent = 'Pause';
btnStep.disabled = true;
runLoop();
}
});
btnReset.addEventListener('click', reset);
rainSlider.addEventListener('input', () => { rainVal.textContent = rainSlider.value; });
reset();
Drag the Rain Rate slider to control how fast the water rises. A slow rain gives the search more time to explore; a fast rain rushes the dot to higher ground before it has mapped the terrain. The best setting depends on the landscape — tuning the rain rate is the main practical knob on the Great Deluge.
Comments
Loading comments...