Here is a tiny level. Three blocks stand in the way, and your job is to knock down all of them with the bird's flight. Drag the angle and power sliders, then launch and watch the parabola curve under gravity.
<p class="hint">Three blocks block the way. Tune the <b>angle</b> and <b>power</b>, then launch the bird and watch the parabola. Checking one shot is instant — finding an angle and power that knock down <b>all three</b> is the hard part.</p>
<canvas id="stage" width="480" height="300"></canvas>
<div class="controls">
<label>Angle <span id="angVal">45°</span>
<input id="angle" type="range" min="10" max="80" value="45" step="1"></label>
<label>Power <span id="powVal">10</span>
<input id="power" type="range" min="6" max="14" value="10" step="0.5"></label>
</div>
<div class="status" id="status">Aim, then launch.</div>
<div class="btns">
<button id="launch" type="button">Launch bird</button>
<button id="solve" type="button">Auto-solve</button>
<button id="reset" type="button" class="ghost">Reset blocks</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 480px; height: auto; background: #eaf3fb;
border: 1px solid #cdd9e3; border-radius: 8px; display: block; }
.controls { display: flex; gap: 1.2rem; flex-wrap: wrap; margin: .6rem 0 .2rem; }
.controls label { font-size: .85rem; font-weight: 600; color: #1d3557;
display: flex; flex-direction: column; gap: .2rem; }
.controls input { width: 150px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Angry Birds "level clearing": choose a launch angle and power so the bird's parabolic flight knocks down EVERY block. Checking one shot is a quick simulation; deciding whether a level CAN be cleared is NP-hard in general (Viglietta, 2016) — here we just sweep an angle x power grid to find a shot.
const cv = document.getElementById('stage'), ctx = cv.getContext('2d');
const W = 480, H = 300, G = 0.45;
const groundY = H - 20, BW = 22;
const x0 = 40, y0 = groundY - 10; // slingshot anchor
const initialBlocks = [
{ x: 250, top: groundY - 70 },
{ x: 300, top: groundY - 110 },
{ x: 360, top: groundY - 50 }
];
let blocks = initialBlocks.map(b => ({ ...b }));
// Simulate one shot. Returns {toppled:Set, path:[[x,y]...]}.
// A standing block topples when the bird's point enters its rectangle while flying.
function simulate(angleDeg, speed, blockList){
const a = angleDeg * Math.PI / 180;
let vx = Math.cos(a) * speed, vy = -Math.sin(a) * speed;
let x = x0, y = y0;
const toppled = new Set(), path = [[x, y]];
for (let t = 0; t < 2000; t++){
x += vx; y += vy; vy += G;
path.push([x, y]);
for (let i = 0; i < blockList.length; i++){
if (toppled.has(i)) continue;
const b = blockList[i];
if (x >= b.x && x <= b.x + BW && y >= b.top && y <= groundY){
toppled.add(i);
vx *= 0.6; vy = -Math.abs(vy) * 0.4; // lose energy, bounce a little
}
}
if (y > groundY && vy > 0) break; // hit ground
if (x > W + 40) break;
}
return { toppled, path };
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function drawScene(path, toppled){
ctx.clearRect(0, 0, W, H);
// ground
ctx.fillStyle = '#7cb342'; ctx.fillRect(0, groundY, W, H - groundY);
// slingshot
ctx.strokeStyle = '#6d4c2f'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(x0, groundY); ctx.lineTo(x0, y0 - 6); ctx.stroke();
// blocks
blocks.forEach((b, i) => {
const fell = toppled && toppled.has(i);
ctx.fillStyle = fell ? '#c9ccd1' : '#a1632e';
ctx.strokeStyle = fell ? '#adb1b8' : '#7a4a1f'; ctx.lineWidth = 2;
if (fell){ // lying on the ground
ctx.fillRect(b.x - 24, groundY - BW, groundY - b.top, BW);
ctx.strokeRect(b.x - 24, groundY - BW, groundY - b.top, BW);
} else {
ctx.fillRect(b.x, b.top, BW, groundY - b.top);
ctx.strokeRect(b.x, b.top, BW, groundY - b.top);
}
});
// trajectory
if (path && path.length){
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]);
ctx.beginPath();
path.forEach(([px, py], i) => i ? ctx.lineTo(px, py) : ctx.moveTo(px, py));
ctx.stroke(); ctx.setLineDash([]);
const [bx, by] = path[path.length - 1];
ctx.fillStyle = '#e63946'; ctx.beginPath(); ctx.arc(bx, by, 8, 0, 7); ctx.fill();
} else {
ctx.fillStyle = '#e63946'; ctx.beginPath(); ctx.arc(x0, y0 - 6, 8, 0, 7); ctx.fill();
}
}
function shoot(angle, power){
const { toppled, path } = simulate(angle, power, blocks);
drawScene(path, toppled);
if (toppled.size === blocks.length) setStatus(fmt('Cleared! All %length% blocks down.', { length: blocks.length }), 'ok');
else setStatus(fmt('Knocked down %size% of %length%. Try another arc.', { size: toppled.size, length: blocks.length }), 'bad');
return toppled.size;
}
const angle = document.getElementById('angle'), power = document.getElementById('power');
const angVal = document.getElementById('angVal'), powVal = document.getElementById('powVal');
function sync(){ angVal.textContent = angle.value + '°'; powVal.textContent = power.value; }
angle.addEventListener('input', () => { sync(); drawScene(null, null); });
power.addEventListener('input', () => { sync(); drawScene(null, null); });
document.getElementById('launch').addEventListener('click', () => shoot(+angle.value, +power.value));
// Brute-force search: sweep angle x power until a shot clears all blocks.
document.getElementById('solve').addEventListener('click', () => {
let tried = 0, best = null;
for (let ang = 10; ang <= 80 && !best; ang += 1)
for (let sp = 6; sp <= 14; sp += 0.5){
tried++;
if (simulate(ang, sp, blocks).toppled.size === blocks.length){ best = { ang, sp }; break; }
}
if (best){
angle.value = best.ang; power.value = best.sp; sync();
shoot(best.ang, best.sp);
setStatus(fmt('Auto-solve found angle %ang%°, power %sp% after %tried% shots.', { ang: best.ang, sp: best.sp, tried: tried }), 'ok');
console.log('solution: angle', best.ang, 'power', best.sp, '| simulated', tried, 'shots');
} else setStatus('No shot in the search grid clears this level.', 'bad');
});
document.getElementById('reset').addEventListener('click', () => {
blocks = initialBlocks.map(b => ({ ...b }));
drawScene(null, null); setStatus('Aim, then launch.', '');
});
sync(); drawScene(null, null);
<p class="hint">Three blocks block the way. Tune the <b>angle</b> and <b>power</b>, then launch the bird and watch the parabola. Checking one shot is instant — finding an angle and power that knock down <b>all three</b> is the hard part.</p>
<canvas id="stage" width="480" height="300"></canvas>
<div class="controls">
<label>Angle <span id="angVal">45°</span>
<input id="angle" type="range" min="10" max="80" value="45" step="1"></label>
<label>Power <span id="powVal">10</span>
<input id="power" type="range" min="6" max="14" value="10" step="0.5"></label>
</div>
<div class="status" id="status">Aim, then launch.</div>
<div class="btns">
<button id="launch" type="button">Launch bird</button>
<button id="solve" type="button">Auto-solve</button>
<button id="reset" type="button" class="ghost">Reset blocks</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 480px; height: auto; background: #eaf3fb;
border: 1px solid #cdd9e3; border-radius: 8px; display: block; }
.controls { display: flex; gap: 1.2rem; flex-wrap: wrap; margin: .6rem 0 .2rem; }
.controls label { font-size: .85rem; font-weight: 600; color: #1d3557;
display: flex; flex-direction: column; gap: .2rem; }
.controls input { width: 150px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Angry Birds "level clearing": choose a launch angle and power so the bird's parabolic flight knocks down EVERY block. Checking one shot is a quick simulation; deciding whether a level CAN be cleared is NP-hard in general (Viglietta, 2016) — here we just sweep an angle x power grid to find a shot.
const cv = document.getElementById('stage'), ctx = cv.getContext('2d');
const W = 480, H = 300, G = 0.45;
const groundY = H - 20, BW = 22;
const x0 = 40, y0 = groundY - 10; // slingshot anchor
const initialBlocks = [
{ x: 250, top: groundY - 70 },
{ x: 300, top: groundY - 110 },
{ x: 360, top: groundY - 50 }
];
let blocks = initialBlocks.map(b => ({ ...b }));
// Simulate one shot. Returns {toppled:Set, path:[[x,y]...]}.
// A standing block topples when the bird's point enters its rectangle while flying.
function simulate(angleDeg, speed, blockList){
const a = angleDeg * Math.PI / 180;
let vx = Math.cos(a) * speed, vy = -Math.sin(a) * speed;
let x = x0, y = y0;
const toppled = new Set(), path = [[x, y]];
for (let t = 0; t < 2000; t++){
x += vx; y += vy; vy += G;
path.push([x, y]);
for (let i = 0; i < blockList.length; i++){
if (toppled.has(i)) continue;
const b = blockList[i];
if (x >= b.x && x <= b.x + BW && y >= b.top && y <= groundY){
toppled.add(i);
vx *= 0.6; vy = -Math.abs(vy) * 0.4; // lose energy, bounce a little
}
}
if (y > groundY && vy > 0) break; // hit ground
if (x > W + 40) break;
}
return { toppled, path };
}
function setStatus(t, cls){ const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls || ''); }
function drawScene(path, toppled){
ctx.clearRect(0, 0, W, H);
// ground
ctx.fillStyle = '#7cb342'; ctx.fillRect(0, groundY, W, H - groundY);
// slingshot
ctx.strokeStyle = '#6d4c2f'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(x0, groundY); ctx.lineTo(x0, y0 - 6); ctx.stroke();
// blocks
blocks.forEach((b, i) => {
const fell = toppled && toppled.has(i);
ctx.fillStyle = fell ? '#c9ccd1' : '#a1632e';
ctx.strokeStyle = fell ? '#adb1b8' : '#7a4a1f'; ctx.lineWidth = 2;
if (fell){ // lying on the ground
ctx.fillRect(b.x - 24, groundY - BW, groundY - b.top, BW);
ctx.strokeRect(b.x - 24, groundY - BW, groundY - b.top, BW);
} else {
ctx.fillRect(b.x, b.top, BW, groundY - b.top);
ctx.strokeRect(b.x, b.top, BW, groundY - b.top);
}
});
// trajectory
if (path && path.length){
ctx.strokeStyle = '#1d3557'; ctx.lineWidth = 2; ctx.setLineDash([4, 4]);
ctx.beginPath();
path.forEach(([px, py], i) => i ? ctx.lineTo(px, py) : ctx.moveTo(px, py));
ctx.stroke(); ctx.setLineDash([]);
const [bx, by] = path[path.length - 1];
ctx.fillStyle = '#e63946'; ctx.beginPath(); ctx.arc(bx, by, 8, 0, 7); ctx.fill();
} else {
ctx.fillStyle = '#e63946'; ctx.beginPath(); ctx.arc(x0, y0 - 6, 8, 0, 7); ctx.fill();
}
}
function shoot(angle, power){
const { toppled, path } = simulate(angle, power, blocks);
drawScene(path, toppled);
if (toppled.size === blocks.length) setStatus(fmt('Cleared! All %length% blocks down.', { length: blocks.length }), 'ok');
else setStatus(fmt('Knocked down %size% of %length%. Try another arc.', { size: toppled.size, length: blocks.length }), 'bad');
return toppled.size;
}
const angle = document.getElementById('angle'), power = document.getElementById('power');
const angVal = document.getElementById('angVal'), powVal = document.getElementById('powVal');
function sync(){ angVal.textContent = angle.value + '°'; powVal.textContent = power.value; }
angle.addEventListener('input', () => { sync(); drawScene(null, null); });
power.addEventListener('input', () => { sync(); drawScene(null, null); });
document.getElementById('launch').addEventListener('click', () => shoot(+angle.value, +power.value));
// Brute-force search: sweep angle x power until a shot clears all blocks.
document.getElementById('solve').addEventListener('click', () => {
let tried = 0, best = null;
for (let ang = 10; ang <= 80 && !best; ang += 1)
for (let sp = 6; sp <= 14; sp += 0.5){
tried++;
if (simulate(ang, sp, blocks).toppled.size === blocks.length){ best = { ang, sp }; break; }
}
if (best){
angle.value = best.ang; power.value = best.sp; sync();
shoot(best.ang, best.sp);
setStatus(fmt('Auto-solve found angle %ang%°, power %sp% after %tried% shots.', { ang: best.ang, sp: best.sp, tried: tried }), 'ok');
console.log('solution: angle', best.ang, 'power', best.sp, '| simulated', tried, 'shots');
} else setStatus('No shot in the search grid clears this level.', 'bad');
});
document.getElementById('reset').addEventListener('click', () => {
blocks = initialBlocks.map(b => ({ ...b }));
drawScene(null, null); setStatus('Aim, then launch.', '');
});
sync(); drawScene(null, null);
Notice the asymmetry. Checking a shot is effortless: launch it, watch the arc, see which blocks fall. Finding an angle and power that clears every block is the hard part — press Auto-solve and the computer simply tries thousands of angle-and-power combinations until one works. Here it searches a small grid; in a real level, with stacked blocks and chain reactions, the space of plans explodes.
Comments
Loading comments...