Here is a tiny road network. One hundred drivers travel from Start to End. Two roads get slower the more drivers use them (their time equals the fraction of drivers on them); two are fixed at one hour. There is also a free shortcut in the middle.
<p class="hint">100 drivers go from <b>Start</b> to <b>End</b>. Two roads change with
traffic (their time = the fraction of drivers using them); two are fixed at 1 hour.
Toggle the free shortcut, then let drivers act <b>selfishly</b> or coordinate for the
<b>social optimum</b>. Watch what the shortcut does.</p>
<div class="net">
<svg viewBox="0 0 320 200" id="svg">
<line class="edge" id="e_sa" x1="40" y1="100" x2="160" y2="40"/>
<line class="edge" id="e_at" x1="160" y1="40" x2="280" y2="100"/>
<line class="edge" id="e_sb" x1="40" y1="100" x2="160" y2="160"/>
<line class="edge" id="e_bt" x1="160" y1="160" x2="280" y2="100"/>
<line class="edge cross" id="e_ab" x1="160" y1="40" x2="160" y2="160"/>
<circle class="node" cx="40" cy="100" r="16"/><text x="40" y="104">S</text>
<circle class="node" cx="160" cy="40" r="16"/><text x="160" y="44">A</text>
<circle class="node" cx="160" cy="160" r="16"/><text x="160" y="164">B</text>
<circle class="node end" cx="280" cy="100" r="16"/><text x="280" y="104">T</text>
<text class="lbl" id="l_sa" x="92" y="60">load</text>
<text class="lbl" id="l_at" x="228" y="60">1 h</text>
<text class="lbl" id="l_sb" x="92" y="148">1 h</text>
<text class="lbl" id="l_bt" x="228" y="148">load</text>
<text class="lbl" id="l_ab" x="172" y="104">free</text>
</svg>
</div>
<label class="toggle"><input type="checkbox" id="shortcut" checked> Shortcut road A→B open (free)</label>
<div class="readout" id="readout"></div>
<div class="btns">
<button id="selfish" type="button">Let drivers be selfish</button>
<button id="social" type="button">Social optimum</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 .6rem; line-height: 1.45; }
.net { display: flex; justify-content: center; }
svg { width: 100%; max-width: 360px; height: auto; }
.edge { stroke: #adb1b8; stroke-width: 6; stroke-linecap: round; }
.edge.hot { stroke: #e63946; }
.edge.off { stroke-dasharray: 4 6; opacity: .35; }
.node { fill: #e8eef3; stroke: #1d3557; stroke-width: 2; }
.node.end { fill: #1d3557; }
text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; }
.node.end + text { fill: #fff; }
.lbl { font: 600 10px system-ui, sans-serif; fill: #555; }
.toggle { display: block; font-size: .9rem; margin: .4rem 0; cursor: pointer; }
.readout { font-size: .95rem; line-height: 1.6; margin: .5rem 0; min-height: 5.2em;
background: #f4f6f9; border: 1px solid #dde3ea; border-radius: 8px; padding: .6rem .8rem; }
.readout b { color: #1d3557; }
.big { font-weight: 700; }
.warn { color: #c92f3c; font-weight: 700; }
.good { color: #0a7d33; font-weight: 700; }
.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:nth-child(2) { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The "price of anarchy" = (cost when everyone is selfish) / (cost of the
// coordinated optimum). This classic network reproduces Braess’s paradox:
// adding a fast road can make EVERY driver slower at equilibrium.
// Fixed edges (A-T, S-B) cost 1 hour. The shortcut A-B is free.
const N = 100;
function avgTime(top, bottom, cross){
const loadSA = (top + cross) / N; // S->A used by Top + Cross routes
const loadBT = (bottom + cross) / N; // B->T used by Bottom + Cross routes
const tTop = loadSA + 1; // S-A (load) + A-T (1h)
const tBottom = 1 + loadBT; // S-B (1h) + B-T (load)
const tCross = loadSA + loadBT; // S-A (load) + A-B (free) + B-T (load)
return (top*tTop + bottom*tBottom + cross*tCross) / N;
}
// Best-response dynamics: repeatedly move one driver off the slowest route
// onto the fastest, until nobody can improve — a Nash (selfish) equilibrium.
function equilibrium(shortcut){
let top = N/2, bottom = N/2, cross = 0;
for(let i=0; i<4000; i++){
const loadSA=(top+cross)/N, loadBT=(bottom+cross)/N;
const tTop=loadSA+1, tBottom=1+loadBT, tCross = shortcut ? (loadSA+loadBT) : Infinity;
const R=[['top',tTop,top],['bottom',tBottom,bottom],['cross',tCross,cross]];
const used=R.filter(r=>r[2]>0);
const worst=used.reduce((a,b)=>a[1]>b[1]?a:b);
const best=R.reduce((a,b)=>a[1]<b[1]?a:b);
if(worst[1]-best[1] < 1e-9) break;
if(worst[0]==='top')top--; else if(worst[0]==='bottom')bottom--; else cross--;
if(best[0]==='top')top++; else if(best[0]==='bottom')bottom++; else cross++;
}
return {top,bottom,cross,avg:avgTime(top,bottom,cross)};
}
// Social optimum: search every split for the lowest AVERAGE travel time.
function socialOpt(shortcut){
let best={avg:Infinity,top:0,bottom:0,cross:0};
const maxC = shortcut ? N : 0;
for(let cross=0; cross<=maxC; cross++)
for(let top=0; top<=N-cross; top++){
const a=avgTime(top, N-cross-top, cross);
if(a<best.avg) best={top,bottom:N-cross-top,cross,avg:a};
}
return best;
}
const $ = id => document.getElementById(id);
function paint(r){
$('e_ab').classList.toggle('off', !$('shortcut').checked);
// highlight S-A and B-T red when heavily loaded (cross route hammers them)
const loadSA=(r.top+r.cross)/N, loadBT=(r.bottom+r.cross)/N;
$('e_sa').classList.toggle('hot', loadSA>0.75);
$('e_bt').classList.toggle('hot', loadBT>0.75);
}
function show(label, r, baseline){
paint(r);
const t = r.avg.toFixed(2);
let extra='';
if(baseline!=null){
const slower = r.avg > baseline + 1e-9;
extra = slower
? fmt('<br><span class="warn">⚠ Braess’s paradox: the shortcut made everyone slower (%baseline% h → %t% h).</span>', { baseline: baseline.toFixed(2), t: t })
: '<br><span class="good">No paradox here — the shortcut didn’t hurt.</span>';
}
$('readout').innerHTML =
fmt('<b>%label%</b><br>Top route: %top% · Bottom: %bottom% · Shortcut S→A→B→T: %cross%<br>Average travel time: <span class="big">%t% hours</span>%extra%', { label: label, top: r.top, bottom: r.bottom, cross: r.cross, t: t, extra: extra });
}
let lastSocialNoShortcut = null;
$('selfish').addEventListener('click', ()=>{
const sc = $('shortcut').checked;
const r = equilibrium(sc);
// baseline = selfish time WITHOUT the shortcut, to expose the paradox
const base = sc ? equilibrium(false).avg : null;
show('Selfish equilibrium (everyone for themselves)', r, base);
});
$('social').addEventListener('click', ()=>{
const r = socialOpt($('shortcut').checked);
show('Social optimum (coordinated)', r, null);
});
$('shortcut').addEventListener('change', ()=>{ paint({top:50,bottom:50,cross:0}); $('readout').innerHTML='Pick a strategy below.'; });
paint({top:50,bottom:50,cross:0});
$('readout').innerHTML='Pick a strategy below.';
<p class="hint">100 drivers go from <b>Start</b> to <b>End</b>. Two roads change with
traffic (their time = the fraction of drivers using them); two are fixed at 1 hour.
Toggle the free shortcut, then let drivers act <b>selfishly</b> or coordinate for the
<b>social optimum</b>. Watch what the shortcut does.</p>
<div class="net">
<svg viewBox="0 0 320 200" id="svg">
<line class="edge" id="e_sa" x1="40" y1="100" x2="160" y2="40"/>
<line class="edge" id="e_at" x1="160" y1="40" x2="280" y2="100"/>
<line class="edge" id="e_sb" x1="40" y1="100" x2="160" y2="160"/>
<line class="edge" id="e_bt" x1="160" y1="160" x2="280" y2="100"/>
<line class="edge cross" id="e_ab" x1="160" y1="40" x2="160" y2="160"/>
<circle class="node" cx="40" cy="100" r="16"/><text x="40" y="104">S</text>
<circle class="node" cx="160" cy="40" r="16"/><text x="160" y="44">A</text>
<circle class="node" cx="160" cy="160" r="16"/><text x="160" y="164">B</text>
<circle class="node end" cx="280" cy="100" r="16"/><text x="280" y="104">T</text>
<text class="lbl" id="l_sa" x="92" y="60">load</text>
<text class="lbl" id="l_at" x="228" y="60">1 h</text>
<text class="lbl" id="l_sb" x="92" y="148">1 h</text>
<text class="lbl" id="l_bt" x="228" y="148">load</text>
<text class="lbl" id="l_ab" x="172" y="104">free</text>
</svg>
</div>
<label class="toggle"><input type="checkbox" id="shortcut" checked> Shortcut road A→B open (free)</label>
<div class="readout" id="readout"></div>
<div class="btns">
<button id="selfish" type="button">Let drivers be selfish</button>
<button id="social" type="button">Social optimum</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 .6rem; line-height: 1.45; }
.net { display: flex; justify-content: center; }
svg { width: 100%; max-width: 360px; height: auto; }
.edge { stroke: #adb1b8; stroke-width: 6; stroke-linecap: round; }
.edge.hot { stroke: #e63946; }
.edge.off { stroke-dasharray: 4 6; opacity: .35; }
.node { fill: #e8eef3; stroke: #1d3557; stroke-width: 2; }
.node.end { fill: #1d3557; }
text { font: 700 13px ui-monospace, monospace; fill: #1d3557; text-anchor: middle; }
.node.end + text { fill: #fff; }
.lbl { font: 600 10px system-ui, sans-serif; fill: #555; }
.toggle { display: block; font-size: .9rem; margin: .4rem 0; cursor: pointer; }
.readout { font-size: .95rem; line-height: 1.6; margin: .5rem 0; min-height: 5.2em;
background: #f4f6f9; border: 1px solid #dde3ea; border-radius: 8px; padding: .6rem .8rem; }
.readout b { color: #1d3557; }
.big { font-weight: 700; }
.warn { color: #c92f3c; font-weight: 700; }
.good { color: #0a7d33; font-weight: 700; }
.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:nth-child(2) { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The "price of anarchy" = (cost when everyone is selfish) / (cost of the
// coordinated optimum). This classic network reproduces Braess’s paradox:
// adding a fast road can make EVERY driver slower at equilibrium.
// Fixed edges (A-T, S-B) cost 1 hour. The shortcut A-B is free.
const N = 100;
function avgTime(top, bottom, cross){
const loadSA = (top + cross) / N; // S->A used by Top + Cross routes
const loadBT = (bottom + cross) / N; // B->T used by Bottom + Cross routes
const tTop = loadSA + 1; // S-A (load) + A-T (1h)
const tBottom = 1 + loadBT; // S-B (1h) + B-T (load)
const tCross = loadSA + loadBT; // S-A (load) + A-B (free) + B-T (load)
return (top*tTop + bottom*tBottom + cross*tCross) / N;
}
// Best-response dynamics: repeatedly move one driver off the slowest route
// onto the fastest, until nobody can improve — a Nash (selfish) equilibrium.
function equilibrium(shortcut){
let top = N/2, bottom = N/2, cross = 0;
for(let i=0; i<4000; i++){
const loadSA=(top+cross)/N, loadBT=(bottom+cross)/N;
const tTop=loadSA+1, tBottom=1+loadBT, tCross = shortcut ? (loadSA+loadBT) : Infinity;
const R=[['top',tTop,top],['bottom',tBottom,bottom],['cross',tCross,cross]];
const used=R.filter(r=>r[2]>0);
const worst=used.reduce((a,b)=>a[1]>b[1]?a:b);
const best=R.reduce((a,b)=>a[1]<b[1]?a:b);
if(worst[1]-best[1] < 1e-9) break;
if(worst[0]==='top')top--; else if(worst[0]==='bottom')bottom--; else cross--;
if(best[0]==='top')top++; else if(best[0]==='bottom')bottom++; else cross++;
}
return {top,bottom,cross,avg:avgTime(top,bottom,cross)};
}
// Social optimum: search every split for the lowest AVERAGE travel time.
function socialOpt(shortcut){
let best={avg:Infinity,top:0,bottom:0,cross:0};
const maxC = shortcut ? N : 0;
for(let cross=0; cross<=maxC; cross++)
for(let top=0; top<=N-cross; top++){
const a=avgTime(top, N-cross-top, cross);
if(a<best.avg) best={top,bottom:N-cross-top,cross,avg:a};
}
return best;
}
const $ = id => document.getElementById(id);
function paint(r){
$('e_ab').classList.toggle('off', !$('shortcut').checked);
// highlight S-A and B-T red when heavily loaded (cross route hammers them)
const loadSA=(r.top+r.cross)/N, loadBT=(r.bottom+r.cross)/N;
$('e_sa').classList.toggle('hot', loadSA>0.75);
$('e_bt').classList.toggle('hot', loadBT>0.75);
}
function show(label, r, baseline){
paint(r);
const t = r.avg.toFixed(2);
let extra='';
if(baseline!=null){
const slower = r.avg > baseline + 1e-9;
extra = slower
? fmt('<br><span class="warn">⚠ Braess’s paradox: the shortcut made everyone slower (%baseline% h → %t% h).</span>', { baseline: baseline.toFixed(2), t: t })
: '<br><span class="good">No paradox here — the shortcut didn’t hurt.</span>';
}
$('readout').innerHTML =
fmt('<b>%label%</b><br>Top route: %top% · Bottom: %bottom% · Shortcut S→A→B→T: %cross%<br>Average travel time: <span class="big">%t% hours</span>%extra%', { label: label, top: r.top, bottom: r.bottom, cross: r.cross, t: t, extra: extra });
}
let lastSocialNoShortcut = null;
$('selfish').addEventListener('click', ()=>{
const sc = $('shortcut').checked;
const r = equilibrium(sc);
// baseline = selfish time WITHOUT the shortcut, to expose the paradox
const base = sc ? equilibrium(false).avg : null;
show('Selfish equilibrium (everyone for themselves)', r, base);
});
$('social').addEventListener('click', ()=>{
const r = socialOpt($('shortcut').checked);
show('Social optimum (coordinated)', r, null);
});
$('shortcut').addEventListener('change', ()=>{ paint({top:50,bottom:50,cross:0}); $('readout').innerHTML='Pick a strategy below.'; });
paint({top:50,bottom:50,cross:0});
$('readout').innerHTML='Pick a strategy below.';
Comments
Loading comments...