Below are three voters, each ranking three options A, B and C. Reorder anyone's ballot, then press Run majority votes. The demo holds three head-to-head elections: A vs B, B vs C, and A vs C — each decided by simple majority.
<p class="hint">Three voters each rank options <b>A</b>, <b>B</b> and <b>C</b>. Click a ballot to cycle that voter's ranking. Then run three majority head-to-heads — and watch the group's "preference" possibly chase itself in a circle.</p>
<div id="voters" class="voters"></div>
<div class="btns">
<button id="preset" type="button">Load paradox preset</button>
<button id="run" type="button">Run majority votes</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div id="results" class="results"></div>
<div class="status" id="status">Set the ballots, then run the votes.</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; }
.voters { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; margin: .4rem 0 .9rem; }
.voter { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem; text-align: center; }
.voter h4 { margin: 0 0 .5rem; font-size: .85rem; color: #1d3557; }
.ballot { display: flex; flex-direction: column; gap: .3rem; cursor: pointer; user-select: none; }
.rank { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 7px;
font: 700 16px ui-monospace, monospace; color: #1d3557; padding: .35rem; transition: background .1s; }
.ballot:hover .rank { background: #dce6ef; }
.rank .pos { font-size: .65rem; color: #6b7d8f; display: block; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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; }
.results { display: flex; flex-direction: column; gap: .35rem; margin: .4rem 0; }
.match { font: 600 14px system-ui, sans-serif; padding: .4rem .6rem; border-radius: 7px; background: #f1f4f7; }
.match .win { color: #0a7d33; font-weight: 700; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.cycle { color: #c92f3c; }
.status.ok { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Arrow's theorem in miniature: even simple majority rule, applied to three
// honest rankings of three options, can produce a CONDORCET CYCLE — the group
// prefers A>B, B>C and C>A all at once, with no consistent winner. Arrow (1951)
// proved no ranked rule escapes such failures without breaking a fairness axiom.
const OPTS = ['A', 'B', 'C'];
// Each voter is a ranking: index 0 = most preferred.
let voters = [
['A', 'B', 'C'],
['A', 'B', 'C'],
['A', 'B', 'C'],
];
// The classic Condorcet paradox profile.
const PRESET = [
['A', 'B', 'C'],
['B', 'C', 'A'],
['C', 'A', 'B'],
];
function renderVoters() {
const wrap = document.getElementById('voters');
wrap.innerHTML = '';
voters.forEach((ballot, vi) => {
const card = document.createElement('div');
card.className = 'voter';
card.innerHTML = fmt('<h4>Voter %vi%%v%</h4>', { vi: vi, v: 1 });
const bal = document.createElement('div');
bal.className = 'ballot';
ballot.forEach((opt, pos) => {
const r = document.createElement('div');
r.className = 'rank';
const label = pos === 0 ? '1st' : pos === 1 ? '2nd' : '3rd';
r.innerHTML = '<span class="pos">' + label + '</span>' + opt;
bal.appendChild(r);
});
// Click cycles the ballot (rotate the ranking) so users can explore profiles.
bal.addEventListener('click', () => {
voters[vi] = [ballot[2], ballot[0], ballot[1]];
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Set the ballots, then run the votes.', '');
});
card.appendChild(bal);
wrap.appendChild(card);
});
}
// Does voter rank x above y?
function prefers(ballot, x, y) {
return ballot.indexOf(x) < ballot.indexOf(y);
}
// Majority winner of a head-to-head; returns the winning option (no ties possible with 3 voters).
function headToHead(x, y) {
let xv = 0, yv = 0;
voters.forEach(b => { prefers(b, x, y) ? xv++ : yv++; });
return xv > yv ? x : y;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function run() {
const pairs = [['A', 'B'], ['B', 'C'], ['A', 'C']];
const winners = {};
const res = document.getElementById('results');
res.innerHTML = '';
pairs.forEach(([x, y]) => {
const w = headToHead(x, y);
winners[x + y] = w;
const div = document.createElement('div');
div.className = 'match';
div.innerHTML = fmt('%x% vs %y% → majority prefers <span class="win">%w%</span>', { x: x, y: y, w: w });
res.appendChild(div);
});
// Build the group preference graph and look for a 3-cycle.
const abW = winners['AB'], bcW = winners['BC'], acW = winners['AC'];
// Cycle if A>B, B>C, and C>A (or the mirror image).
const cycle1 = abW === 'A' && bcW === 'B' && acW === 'C';
const cycle2 = abW === 'B' && bcW === 'C' && acW === 'A';
if (cycle1 || cycle2) {
setStatus('↻ Condorcet cycle! The group prefers a chain that loops back — no consistent winner.', 'cycle');
console.log('Cycle detected:', winners);
} else {
// Find the option that beats both others = the Condorcet winner.
const beats = o => OPTS.filter(p => p !== o).every(p => {
const key = [o, p].sort().join('');
return winners[key] === o;
});
const champ = OPTS.find(beats);
setStatus(fmt('✓ Transitive: %champ% beats everyone — a clear Condorcet winner.', { champ: champ }), 'ok');
console.log('Winner:', champ, winners);
}
}
document.getElementById('preset').addEventListener('click', () => {
voters = PRESET.map(b => b.slice());
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Paradox preset loaded. Now run the votes.', '');
});
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
voters = OPTS.map(() => OPTS.slice());
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Set the ballots, then run the votes.', '');
});
renderVoters();
<p class="hint">Three voters each rank options <b>A</b>, <b>B</b> and <b>C</b>. Click a ballot to cycle that voter's ranking. Then run three majority head-to-heads — and watch the group's "preference" possibly chase itself in a circle.</p>
<div id="voters" class="voters"></div>
<div class="btns">
<button id="preset" type="button">Load paradox preset</button>
<button id="run" type="button">Run majority votes</button>
<button id="reset" type="button" class="ghost">Clear</button>
</div>
<div id="results" class="results"></div>
<div class="status" id="status">Set the ballots, then run the votes.</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; }
.voters { display: grid; grid-template-columns: repeat(3, 1fr); gap: .6rem; margin: .4rem 0 .9rem; }
.voter { border: 1px solid #cdd9e3; border-radius: 10px; padding: .6rem; text-align: center; }
.voter h4 { margin: 0 0 .5rem; font-size: .85rem; color: #1d3557; }
.ballot { display: flex; flex-direction: column; gap: .3rem; cursor: pointer; user-select: none; }
.rank { background: #e8eef3; border: 1px solid #cdd9e3; border-radius: 7px;
font: 700 16px ui-monospace, monospace; color: #1d3557; padding: .35rem; transition: background .1s; }
.ballot:hover .rank { background: #dce6ef; }
.rank .pos { font-size: .65rem; color: #6b7d8f; display: block; font-weight: 600; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
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; }
.results { display: flex; flex-direction: column; gap: .35rem; margin: .4rem 0; }
.match { font: 600 14px system-ui, sans-serif; padding: .4rem .6rem; border-radius: 7px; background: #f1f4f7; }
.match .win { color: #0a7d33; font-weight: 700; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.cycle { color: #c92f3c; }
.status.ok { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Arrow's theorem in miniature: even simple majority rule, applied to three
// honest rankings of three options, can produce a CONDORCET CYCLE — the group
// prefers A>B, B>C and C>A all at once, with no consistent winner. Arrow (1951)
// proved no ranked rule escapes such failures without breaking a fairness axiom.
const OPTS = ['A', 'B', 'C'];
// Each voter is a ranking: index 0 = most preferred.
let voters = [
['A', 'B', 'C'],
['A', 'B', 'C'],
['A', 'B', 'C'],
];
// The classic Condorcet paradox profile.
const PRESET = [
['A', 'B', 'C'],
['B', 'C', 'A'],
['C', 'A', 'B'],
];
function renderVoters() {
const wrap = document.getElementById('voters');
wrap.innerHTML = '';
voters.forEach((ballot, vi) => {
const card = document.createElement('div');
card.className = 'voter';
card.innerHTML = fmt('<h4>Voter %vi%%v%</h4>', { vi: vi, v: 1 });
const bal = document.createElement('div');
bal.className = 'ballot';
ballot.forEach((opt, pos) => {
const r = document.createElement('div');
r.className = 'rank';
const label = pos === 0 ? '1st' : pos === 1 ? '2nd' : '3rd';
r.innerHTML = '<span class="pos">' + label + '</span>' + opt;
bal.appendChild(r);
});
// Click cycles the ballot (rotate the ranking) so users can explore profiles.
bal.addEventListener('click', () => {
voters[vi] = [ballot[2], ballot[0], ballot[1]];
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Set the ballots, then run the votes.', '');
});
card.appendChild(bal);
wrap.appendChild(card);
});
}
// Does voter rank x above y?
function prefers(ballot, x, y) {
return ballot.indexOf(x) < ballot.indexOf(y);
}
// Majority winner of a head-to-head; returns the winning option (no ties possible with 3 voters).
function headToHead(x, y) {
let xv = 0, yv = 0;
voters.forEach(b => { prefers(b, x, y) ? xv++ : yv++; });
return xv > yv ? x : y;
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t;
s.className = 'status ' + cls;
}
function run() {
const pairs = [['A', 'B'], ['B', 'C'], ['A', 'C']];
const winners = {};
const res = document.getElementById('results');
res.innerHTML = '';
pairs.forEach(([x, y]) => {
const w = headToHead(x, y);
winners[x + y] = w;
const div = document.createElement('div');
div.className = 'match';
div.innerHTML = fmt('%x% vs %y% → majority prefers <span class="win">%w%</span>', { x: x, y: y, w: w });
res.appendChild(div);
});
// Build the group preference graph and look for a 3-cycle.
const abW = winners['AB'], bcW = winners['BC'], acW = winners['AC'];
// Cycle if A>B, B>C, and C>A (or the mirror image).
const cycle1 = abW === 'A' && bcW === 'B' && acW === 'C';
const cycle2 = abW === 'B' && bcW === 'C' && acW === 'A';
if (cycle1 || cycle2) {
setStatus('↻ Condorcet cycle! The group prefers a chain that loops back — no consistent winner.', 'cycle');
console.log('Cycle detected:', winners);
} else {
// Find the option that beats both others = the Condorcet winner.
const beats = o => OPTS.filter(p => p !== o).every(p => {
const key = [o, p].sort().join('');
return winners[key] === o;
});
const champ = OPTS.find(beats);
setStatus(fmt('✓ Transitive: %champ% beats everyone — a clear Condorcet winner.', { champ: champ }), 'ok');
console.log('Winner:', champ, winners);
}
}
document.getElementById('preset').addEventListener('click', () => {
voters = PRESET.map(b => b.slice());
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Paradox preset loaded. Now run the votes.', '');
});
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', () => {
voters = OPTS.map(() => OPTS.slice());
renderVoters();
document.getElementById('results').innerHTML = '';
setStatus('Set the ballots, then run the votes.', '');
});
renderVoters();
Comments
Loading comments...