Choose a number of boxes, then keep dropping items one by one. The moment you place the (boxes + 1)-th item, the principle guarantees at least one collision — no matter where you drop them.
<p class="hint">Choose the number of boxes, then drop items one by one. The moment you exceed the box count, a <b>collision is guaranteed</b> — no matter where you drop.</p>
<div class="controls">
<label>Boxes: <select id="boxCount">
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7">7</option>
</select></label>
<button id="dropBtn" type="button">Drop item</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="arena" class="arena"></div>
<div class="status" id="status">Drop items to fill the boxes.</div>
<div id="counter" class="counter"></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 .8rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .9rem; }
label { font-size: .9rem; }
select { font: inherit; padding: .2rem .4rem; border: 1px solid #ccc; border-radius: 6px; }
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; }
button:disabled { opacity: .4; cursor: default; }
.arena { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .6rem; }
.box { flex: 1 1 60px; min-width: 52px; border: 2px solid #8aa8c2; border-radius: 10px;
padding: 6px 4px 4px; background: #eef3f7; display: flex; flex-direction: column;
align-items: center; gap: 4px; min-height: 60px; transition: background .2s, border-color .2s; }
.box.collision { background: #fde8ea; border-color: #e63946; }
.box-label { font-size: .7rem; color: #556; font-weight: 600; letter-spacing: .04em; }
.item { width: 24px; height: 24px; border-radius: 50%; background: #1d3557;
display: flex; align-items: center; justify-content: center;
font-size: .65rem; color: #fff; font-weight: 700; animation: pop .15s ease; }
.item.new { background: #e63946; }
@keyframes pop { from { transform: scale(.4); opacity: .3; } to { transform: scale(1); opacity: 1; } }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.status.ok { color: #0a7d33; }
.status.warn { color: #c92f3c; }
.counter { font-size: .85rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pigeonhole demo: drop items randomly into boxes.
// The principle guarantees: once items > boxes, some box holds >= 2.
// We verify this invariant after every drop.
let boxes = []; // boxes[i] = count of items in box i
let totalItems = 0;
let numBoxes = 5;
let collisionOccurred = false;
function init() {
numBoxes = parseInt(document.getElementById('boxCount').value, 10);
boxes = new Array(numBoxes).fill(0);
totalItems = 0;
collisionOccurred = false;
document.getElementById('dropBtn').disabled = false;
renderArena();
setStatus('Drop items to fill the boxes.', '');
updateCounter();
}
function renderArena() {
const arena = document.getElementById('arena');
arena.innerHTML = '';
boxes.forEach((count, i) => {
const boxEl = document.createElement('div');
boxEl.className = 'box' + (count >= 2 ? ' collision' : '');
const label = document.createElement('div');
label.className = 'box-label';
label.textContent = fmt('Box %i%%v%', { i: i, v: 1 });
boxEl.appendChild(label);
for (let j = 0; j < count; j++) {
const item = document.createElement('div');
item.className = 'item' + (j === count - 1 && count >= 2 ? ' new' : '');
item.textContent = '' + (totalItems - count + j + 1);
boxEl.appendChild(item);
}
arena.appendChild(boxEl);
});
}
function dropItem() {
// Choose a random box
const target = Math.floor(Math.random() * numBoxes);
boxes[target]++;
totalItems++;
renderArena();
updateCounter();
// Check invariant: if items > boxes, collision MUST exist
const hasCollision = boxes.some(c => c >= 2);
if (hasCollision && !collisionOccurred) {
collisionOccurred = true;
const collisionBox = boxes.findIndex(c => c >= 2) + 1;
setStatus(
fmt('✓ Collision in box %collisionBox%! The pigeonhole principle guaranteed this.', { collisionBox: collisionBox }),
'warn'
);
// Verify the invariant is correct
console.assert(
totalItems > numBoxes || hasCollision,
'Invariant: collision must exist when items > boxes'
);
} else if (!hasCollision) {
if (totalItems === numBoxes) {
setStatus('Every box has exactly one item — one more drop must collide.', 'ok');
} else {
setStatus('Drop items to fill the boxes.', '');
}
}
// Verify the core claim: totalItems > numBoxes => collision exists
if (totalItems > numBoxes) {
console.assert(hasCollision, 'Pigeonhole: items > boxes must imply collision');
}
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + cls;
}
function updateCounter() {
document.getElementById('counter').textContent =
fmt('Items placed: %totalItems% | Boxes: %numBoxes% | Collision guaranteed when items > %numBoxes%', { totalItems: totalItems, numBoxes: numBoxes });
}
document.getElementById('dropBtn').addEventListener('click', dropItem);
document.getElementById('resetBtn').addEventListener('click', init);
document.getElementById('boxCount').addEventListener('change', init);
init();
<p class="hint">Choose the number of boxes, then drop items one by one. The moment you exceed the box count, a <b>collision is guaranteed</b> — no matter where you drop.</p>
<div class="controls">
<label>Boxes: <select id="boxCount">
<option value="3">3</option>
<option value="4">4</option>
<option value="5" selected>5</option>
<option value="6">6</option>
<option value="7">7</option>
</select></label>
<button id="dropBtn" type="button">Drop item</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="arena" class="arena"></div>
<div class="status" id="status">Drop items to fill the boxes.</div>
<div id="counter" class="counter"></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 .8rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .9rem; }
label { font-size: .9rem; }
select { font: inherit; padding: .2rem .4rem; border: 1px solid #ccc; border-radius: 6px; }
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; }
button:disabled { opacity: .4; cursor: default; }
.arena { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: .6rem; }
.box { flex: 1 1 60px; min-width: 52px; border: 2px solid #8aa8c2; border-radius: 10px;
padding: 6px 4px 4px; background: #eef3f7; display: flex; flex-direction: column;
align-items: center; gap: 4px; min-height: 60px; transition: background .2s, border-color .2s; }
.box.collision { background: #fde8ea; border-color: #e63946; }
.box-label { font-size: .7rem; color: #556; font-weight: 600; letter-spacing: .04em; }
.item { width: 24px; height: 24px; border-radius: 50%; background: #1d3557;
display: flex; align-items: center; justify-content: center;
font-size: .65rem; color: #fff; font-weight: 700; animation: pop .15s ease; }
.item.new { background: #e63946; }
@keyframes pop { from { transform: scale(.4); opacity: .3; } to { transform: scale(1); opacity: 1; } }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.status.ok { color: #0a7d33; }
.status.warn { color: #c92f3c; }
.counter { font-size: .85rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pigeonhole demo: drop items randomly into boxes.
// The principle guarantees: once items > boxes, some box holds >= 2.
// We verify this invariant after every drop.
let boxes = []; // boxes[i] = count of items in box i
let totalItems = 0;
let numBoxes = 5;
let collisionOccurred = false;
function init() {
numBoxes = parseInt(document.getElementById('boxCount').value, 10);
boxes = new Array(numBoxes).fill(0);
totalItems = 0;
collisionOccurred = false;
document.getElementById('dropBtn').disabled = false;
renderArena();
setStatus('Drop items to fill the boxes.', '');
updateCounter();
}
function renderArena() {
const arena = document.getElementById('arena');
arena.innerHTML = '';
boxes.forEach((count, i) => {
const boxEl = document.createElement('div');
boxEl.className = 'box' + (count >= 2 ? ' collision' : '');
const label = document.createElement('div');
label.className = 'box-label';
label.textContent = fmt('Box %i%%v%', { i: i, v: 1 });
boxEl.appendChild(label);
for (let j = 0; j < count; j++) {
const item = document.createElement('div');
item.className = 'item' + (j === count - 1 && count >= 2 ? ' new' : '');
item.textContent = '' + (totalItems - count + j + 1);
boxEl.appendChild(item);
}
arena.appendChild(boxEl);
});
}
function dropItem() {
// Choose a random box
const target = Math.floor(Math.random() * numBoxes);
boxes[target]++;
totalItems++;
renderArena();
updateCounter();
// Check invariant: if items > boxes, collision MUST exist
const hasCollision = boxes.some(c => c >= 2);
if (hasCollision && !collisionOccurred) {
collisionOccurred = true;
const collisionBox = boxes.findIndex(c => c >= 2) + 1;
setStatus(
fmt('✓ Collision in box %collisionBox%! The pigeonhole principle guaranteed this.', { collisionBox: collisionBox }),
'warn'
);
// Verify the invariant is correct
console.assert(
totalItems > numBoxes || hasCollision,
'Invariant: collision must exist when items > boxes'
);
} else if (!hasCollision) {
if (totalItems === numBoxes) {
setStatus('Every box has exactly one item — one more drop must collide.', 'ok');
} else {
setStatus('Drop items to fill the boxes.', '');
}
}
// Verify the core claim: totalItems > numBoxes => collision exists
if (totalItems > numBoxes) {
console.assert(hasCollision, 'Pigeonhole: items > boxes must imply collision');
}
}
function setStatus(text, cls) {
const s = document.getElementById('status');
s.textContent = text;
s.className = 'status ' + cls;
}
function updateCounter() {
document.getElementById('counter').textContent =
fmt('Items placed: %totalItems% | Boxes: %numBoxes% | Collision guaranteed when items > %numBoxes%', { totalItems: totalItems, numBoxes: numBoxes });
}
document.getElementById('dropBtn').addEventListener('click', dropItem);
document.getElementById('resetBtn').addEventListener('click', init);
document.getElementById('boxCount').addEventListener('change', init);
init();
Notice: when every box already holds one item, the very next drop must create a shared box. You cannot avoid it by being clever about where you click — the math forbids it. This is the essence of the principle: it is a counting argument that leaves no room for strategy.
Comments
Loading comments...