<p class="hint">You have <b>100 troops</b> and 5 battlefields (each worth 1 point). Enter your allocation below — numbers must sum to exactly 100. The computer plays a <b>near-optimal mixed strategy</b>: it samples from the Roberson (2006) equilibrium distribution where every battlefield’s marginal is Uniform[0, 40].</p>
<div id="inputs-area">
<div class="field-row" id="fields"></div>
<div class="sum-display">Troops assigned: <span id="sum-display">0</span> / 100</div>
</div>
<div class="btns">
<button id="fight" type="button">Fight!</button>
<button id="run100" type="button">Run 100 rounds</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div id="result-area" class="hidden">
<table id="battle-table">
<thead><tr><th>Battlefield</th><th>You</th><th>Computer</th><th>Winner</th></tr></thead>
<tbody id="battle-body"></tbody>
<tfoot><tr><th>Battlefields won</th><th id="you-total">—</th><th id="cpu-total">—</th><th id="outcome">—</th></tr></tfoot>
</table>
</div>
<div id="stats-area" class="hidden">
<div class="stats-row">
<span>Your wins: <b id="stat-wins">0</b></span>
<span>Computer wins: <b id="stat-losses">0</b></span>
<span>Ties: <b id="stat-ties">0</b></span>
</div>
<div class="bar-wrap"><div class="bar-fill" id="win-bar"></div></div>
<div class="bar-label" id="bar-label"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.field-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .4rem; }
.field-box { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.field-box label { font-size: .75rem; font-weight: 600; color: #1d3557; }
.field-box input { width: 58px; height: 40px; border: 1.5px solid #adb1b8; border-radius: 8px;
font: 700 16px ui-monospace, monospace; text-align: center; color: #1d3557; }
.field-box input:focus { outline: none; border-color: #1d3557; }
.sum-display { font-size: .85rem; color: #555; margin-bottom: .6rem; }
.sum-display.over { color: #c92f3c; font-weight: 700; }
.sum-display.exact { color: #0a7d33; font-weight: 700; }
.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; }
button:disabled { opacity: .45; cursor: default; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.tie { color: #b56a00; }
.hidden { display: none; }
table { border-collapse: collapse; width: 100%; font-size: .88rem; margin-top: .4rem; }
th, td { padding: .35rem .55rem; border: 1px solid #d0d7e0; text-align: center; }
thead th { background: #e8eef3; color: #1d3557; font-weight: 700; }
tfoot th { background: #e8eef3; font-weight: 700; }
.win-you { background: #d4edda; }
.win-cpu { background: #fde8e8; }
.tie-row { background: #fef9e7; }
.stats-row { display: flex; gap: 1.2rem; flex-wrap: wrap; font-size: .88rem; margin: .6rem 0 .3rem; }
.bar-wrap { height: 10px; background: #e0e6ec; border-radius: 5px; overflow: hidden; margin-bottom: .25rem; }
.bar-fill { height: 100%; background: #2a7f4f; border-radius: 5px; transition: width .4s; }
.bar-label { font-size: .78rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Colonel Blotto demo — 5 battlefields, 100 troops each.
//
// CPU strategy: Roberson (2006) equilibrium for symmetric Blotto with n=5, T=100.
// Reference: Roberson, B. (2006). The Colonel Blotto game. Economic Theory 29(1), 1-24.
//
// In the equilibrium each battlefield’s marginal distribution is Uniform[0, 2T/n] = U[0,40].
// We sample using rejection: draw a,b,c,d ~ U[0,40] independently, set e = 100-a-b-c-d,
// accept if 0 <= e <= 40 (acceptance rate ~60%). Then shuffle all 5 positions.
// This yields exactly U[0,40] marginals and correct independence structure.
//
// Consequence: any FIXED human allocation wins around 45-50% at best.
// Concentrated allocations (e.g. 100-0-0-0-0) lose almost every round.
const N = 5;
const T = 100;
const MAX_PER = (2 * T / N) | 0; // 40
const inputs = [];
let stats = { wins: 0, losses: 0, ties: 0 };
// Build input fields
const fieldsDiv = document.getElementById('fields');
for (let i = 0; i < N; i++) {
const box = document.createElement('div');
box.className = 'field-box';
const lbl = document.createElement('label');
lbl.textContent = fmt('BF %i%%v%', { i: i, v: 1 });
const inp = document.createElement('input');
inp.type = 'number';
inp.min = '0';
inp.max = '100';
inp.value = '20';
box.appendChild(lbl);
box.appendChild(inp);
fieldsDiv.appendChild(box);
inputs.push(inp);
inp.addEventListener('input', updateSum);
}
function getPlayerAlloc() {
return inputs.map(inp => Math.max(0, parseInt(inp.value) || 0));
}
function updateSum() {
const s = getPlayerAlloc().reduce((a, b) => a + b, 0);
const disp = document.getElementById('sum-display');
disp.textContent = s;
disp.parentElement.className = 'sum-display' + (s > T ? ' over' : s === T ? ' exact' : '');
}
// Sample CPU allocation from the Roberson equilibrium (rejection sampling)
function cpuAlloc() {
let raw;
do {
const a = Math.random() * MAX_PER;
const b = Math.random() * MAX_PER;
const c = Math.random() * MAX_PER;
const d = Math.random() * MAX_PER;
const e = T - a - b - c - d;
if (e >= 0 && e <= MAX_PER) raw = [a, b, c, d, e];
} while (!raw);
// Shuffle
for (let i = N - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[raw[i], raw[j]] = [raw[j], raw[i]];
}
// Round to integers summing to T
let alloc = raw.map(v => Math.round(v));
let diff = T - alloc.reduce((a, b) => a + b, 0);
if (diff > 0) { const idx = alloc.indexOf(Math.min(...alloc)); alloc[idx] += diff; }
else if (diff < 0) { const idx = alloc.indexOf(Math.max(...alloc)); alloc[idx] += diff; }
return alloc;
}
function battle(player, cpu) {
return player.map((p, i) => {
const c = cpu[i];
return { player: p, cpu: c, winner: p > c ? 1 : c > p ? -1 : 0 };
});
}
function showResult(results) {
const area = document.getElementById('result-area');
area.classList.remove('hidden');
const tbody = document.getElementById('battle-body');
tbody.innerHTML = '';
let yW = 0, cW = 0;
results.forEach((r, i) => {
const tr = document.createElement('tr');
tr.className = r.winner === 1 ? 'win-you' : r.winner === -1 ? 'win-cpu' : 'tie-row';
const winLabel = r.winner === 1 ? 'You' : r.winner === -1 ? 'CPU' : 'Tie';
tr.innerHTML = fmt('<td><b>BF %i%%v%</b></td><td>%player%</td><td>%cpu%</td><td>%winLabel%</td>', { i: i, v: 1, player: r.player, cpu: r.cpu, winLabel: winLabel });
tbody.appendChild(tr);
if (r.winner === 1) yW++;
else if (r.winner === -1) cW++;
});
document.getElementById('you-total').textContent = yW;
document.getElementById('cpu-total').textContent = cW;
const outcomeEl = document.getElementById('outcome');
if (yW > cW) { outcomeEl.textContent = 'You win!'; setStatus(fmt('You win %yW%-%cW%!', { yW: yW, cW: cW }), 'ok'); stats.wins++; }
else if (cW > yW) { outcomeEl.textContent = 'CPU wins'; setStatus(fmt('CPU wins %cW%-%yW%. No fixed plan beats the equilibrium!', { cW: cW, yW: yW }), 'bad'); stats.losses++; }
else { outcomeEl.textContent = 'Tie'; setStatus(fmt('It’s a tie %yW%-%cW%!', { yW: yW, cW: cW }), 'tie'); stats.ties++; }
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
function updateStatsUI() {
const total = stats.wins + stats.losses + stats.ties;
if (total === 0) return;
document.getElementById('stats-area').classList.remove('hidden');
document.getElementById('stat-wins').textContent = stats.wins;
document.getElementById('stat-losses').textContent = stats.losses;
document.getElementById('stat-ties').textContent = stats.ties;
const pct = Math.round(stats.wins / total * 100);
document.getElementById('win-bar').style.width = pct + '%';
document.getElementById('bar-label').textContent =
fmt('Your win rate: %pct%% over %total% rounds — equilibrium prediction: ~45% for any fixed allocation', { pct: pct, total: total });
}
document.getElementById('fight').addEventListener('click', () => {
const player = getPlayerAlloc();
const s = player.reduce((a, b) => a + b, 0);
if (s !== T) { setStatus(fmt('Troops must sum to exactly 100 (currently %s%).', { s: s }), 'bad'); return; }
const cpu = cpuAlloc();
showResult(battle(player, cpu));
updateStatsUI();
});
document.getElementById('run100').addEventListener('click', () => {
const player = getPlayerAlloc();
const s = player.reduce((a, b) => a + b, 0);
if (s !== T) { setStatus(fmt('Troops must sum to exactly 100 (currently %s%).', { s: s }), 'bad'); return; }
for (let i = 0; i < 100; i++) {
const results = battle(player, cpuAlloc());
const yW = results.filter(r => r.winner === 1).length;
const cW = results.filter(r => r.winner === -1).length;
if (yW > cW) stats.wins++;
else if (cW > yW) stats.losses++;
else stats.ties++;
}
document.getElementById('result-area').classList.add('hidden');
setStatus('Simulated 100 rounds against the Roberson equilibrium strategy.', 'tie');
updateStatsUI();
});
document.getElementById('reset-btn').addEventListener('click', () => {
inputs.forEach(inp => { inp.value = '20'; });
stats = { wins: 0, losses: 0, ties: 0 };
document.getElementById('result-area').classList.add('hidden');
document.getElementById('stats-area').classList.add('hidden');
setStatus('', '');
updateSum();
});
updateSum();
<p class="hint">You have <b>100 troops</b> and 5 battlefields (each worth 1 point). Enter your allocation below — numbers must sum to exactly 100. The computer plays a <b>near-optimal mixed strategy</b>: it samples from the Roberson (2006) equilibrium distribution where every battlefield’s marginal is Uniform[0, 40].</p>
<div id="inputs-area">
<div class="field-row" id="fields"></div>
<div class="sum-display">Troops assigned: <span id="sum-display">0</span> / 100</div>
</div>
<div class="btns">
<button id="fight" type="button">Fight!</button>
<button id="run100" type="button">Run 100 rounds</button>
<button id="reset-btn" type="button" class="ghost">Reset</button>
</div>
<div class="status" id="status"></div>
<div id="result-area" class="hidden">
<table id="battle-table">
<thead><tr><th>Battlefield</th><th>You</th><th>Computer</th><th>Winner</th></tr></thead>
<tbody id="battle-body"></tbody>
<tfoot><tr><th>Battlefields won</th><th id="you-total">—</th><th id="cpu-total">—</th><th id="outcome">—</th></tr></tfoot>
</table>
</div>
<div id="stats-area" class="hidden">
<div class="stats-row">
<span>Your wins: <b id="stat-wins">0</b></span>
<span>Computer wins: <b id="stat-losses">0</b></span>
<span>Ties: <b id="stat-ties">0</b></span>
</div>
<div class="bar-wrap"><div class="bar-fill" id="win-bar"></div></div>
<div class="bar-label" id="bar-label"></div>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.field-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: .4rem; }
.field-box { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.field-box label { font-size: .75rem; font-weight: 600; color: #1d3557; }
.field-box input { width: 58px; height: 40px; border: 1.5px solid #adb1b8; border-radius: 8px;
font: 700 16px ui-monospace, monospace; text-align: center; color: #1d3557; }
.field-box input:focus { outline: none; border-color: #1d3557; }
.sum-display { font-size: .85rem; color: #555; margin-bottom: .6rem; }
.sum-display.over { color: #c92f3c; font-weight: 700; }
.sum-display.exact { color: #0a7d33; font-weight: 700; }
.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; }
button:disabled { opacity: .45; cursor: default; }
.status { font-size: 1rem; font-weight: 600; min-height: 1.4em; margin-bottom: .5rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.tie { color: #b56a00; }
.hidden { display: none; }
table { border-collapse: collapse; width: 100%; font-size: .88rem; margin-top: .4rem; }
th, td { padding: .35rem .55rem; border: 1px solid #d0d7e0; text-align: center; }
thead th { background: #e8eef3; color: #1d3557; font-weight: 700; }
tfoot th { background: #e8eef3; font-weight: 700; }
.win-you { background: #d4edda; }
.win-cpu { background: #fde8e8; }
.tie-row { background: #fef9e7; }
.stats-row { display: flex; gap: 1.2rem; flex-wrap: wrap; font-size: .88rem; margin: .6rem 0 .3rem; }
.bar-wrap { height: 10px; background: #e0e6ec; border-radius: 5px; overflow: hidden; margin-bottom: .25rem; }
.bar-fill { height: 100%; background: #2a7f4f; border-radius: 5px; transition: width .4s; }
.bar-label { font-size: .78rem; color: #555; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Colonel Blotto demo — 5 battlefields, 100 troops each.
//
// CPU strategy: Roberson (2006) equilibrium for symmetric Blotto with n=5, T=100.
// Reference: Roberson, B. (2006). The Colonel Blotto game. Economic Theory 29(1), 1-24.
//
// In the equilibrium each battlefield’s marginal distribution is Uniform[0, 2T/n] = U[0,40].
// We sample using rejection: draw a,b,c,d ~ U[0,40] independently, set e = 100-a-b-c-d,
// accept if 0 <= e <= 40 (acceptance rate ~60%). Then shuffle all 5 positions.
// This yields exactly U[0,40] marginals and correct independence structure.
//
// Consequence: any FIXED human allocation wins around 45-50% at best.
// Concentrated allocations (e.g. 100-0-0-0-0) lose almost every round.
const N = 5;
const T = 100;
const MAX_PER = (2 * T / N) | 0; // 40
const inputs = [];
let stats = { wins: 0, losses: 0, ties: 0 };
// Build input fields
const fieldsDiv = document.getElementById('fields');
for (let i = 0; i < N; i++) {
const box = document.createElement('div');
box.className = 'field-box';
const lbl = document.createElement('label');
lbl.textContent = fmt('BF %i%%v%', { i: i, v: 1 });
const inp = document.createElement('input');
inp.type = 'number';
inp.min = '0';
inp.max = '100';
inp.value = '20';
box.appendChild(lbl);
box.appendChild(inp);
fieldsDiv.appendChild(box);
inputs.push(inp);
inp.addEventListener('input', updateSum);
}
function getPlayerAlloc() {
return inputs.map(inp => Math.max(0, parseInt(inp.value) || 0));
}
function updateSum() {
const s = getPlayerAlloc().reduce((a, b) => a + b, 0);
const disp = document.getElementById('sum-display');
disp.textContent = s;
disp.parentElement.className = 'sum-display' + (s > T ? ' over' : s === T ? ' exact' : '');
}
// Sample CPU allocation from the Roberson equilibrium (rejection sampling)
function cpuAlloc() {
let raw;
do {
const a = Math.random() * MAX_PER;
const b = Math.random() * MAX_PER;
const c = Math.random() * MAX_PER;
const d = Math.random() * MAX_PER;
const e = T - a - b - c - d;
if (e >= 0 && e <= MAX_PER) raw = [a, b, c, d, e];
} while (!raw);
// Shuffle
for (let i = N - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[raw[i], raw[j]] = [raw[j], raw[i]];
}
// Round to integers summing to T
let alloc = raw.map(v => Math.round(v));
let diff = T - alloc.reduce((a, b) => a + b, 0);
if (diff > 0) { const idx = alloc.indexOf(Math.min(...alloc)); alloc[idx] += diff; }
else if (diff < 0) { const idx = alloc.indexOf(Math.max(...alloc)); alloc[idx] += diff; }
return alloc;
}
function battle(player, cpu) {
return player.map((p, i) => {
const c = cpu[i];
return { player: p, cpu: c, winner: p > c ? 1 : c > p ? -1 : 0 };
});
}
function showResult(results) {
const area = document.getElementById('result-area');
area.classList.remove('hidden');
const tbody = document.getElementById('battle-body');
tbody.innerHTML = '';
let yW = 0, cW = 0;
results.forEach((r, i) => {
const tr = document.createElement('tr');
tr.className = r.winner === 1 ? 'win-you' : r.winner === -1 ? 'win-cpu' : 'tie-row';
const winLabel = r.winner === 1 ? 'You' : r.winner === -1 ? 'CPU' : 'Tie';
tr.innerHTML = fmt('<td><b>BF %i%%v%</b></td><td>%player%</td><td>%cpu%</td><td>%winLabel%</td>', { i: i, v: 1, player: r.player, cpu: r.cpu, winLabel: winLabel });
tbody.appendChild(tr);
if (r.winner === 1) yW++;
else if (r.winner === -1) cW++;
});
document.getElementById('you-total').textContent = yW;
document.getElementById('cpu-total').textContent = cW;
const outcomeEl = document.getElementById('outcome');
if (yW > cW) { outcomeEl.textContent = 'You win!'; setStatus(fmt('You win %yW%-%cW%!', { yW: yW, cW: cW }), 'ok'); stats.wins++; }
else if (cW > yW) { outcomeEl.textContent = 'CPU wins'; setStatus(fmt('CPU wins %cW%-%yW%. No fixed plan beats the equilibrium!', { cW: cW, yW: yW }), 'bad'); stats.losses++; }
else { outcomeEl.textContent = 'Tie'; setStatus(fmt('It’s a tie %yW%-%cW%!', { yW: yW, cW: cW }), 'tie'); stats.ties++; }
}
function setStatus(t, cls) {
const s = document.getElementById('status');
s.textContent = t; s.className = 'status ' + (cls || '');
}
function updateStatsUI() {
const total = stats.wins + stats.losses + stats.ties;
if (total === 0) return;
document.getElementById('stats-area').classList.remove('hidden');
document.getElementById('stat-wins').textContent = stats.wins;
document.getElementById('stat-losses').textContent = stats.losses;
document.getElementById('stat-ties').textContent = stats.ties;
const pct = Math.round(stats.wins / total * 100);
document.getElementById('win-bar').style.width = pct + '%';
document.getElementById('bar-label').textContent =
fmt('Your win rate: %pct%% over %total% rounds — equilibrium prediction: ~45% for any fixed allocation', { pct: pct, total: total });
}
document.getElementById('fight').addEventListener('click', () => {
const player = getPlayerAlloc();
const s = player.reduce((a, b) => a + b, 0);
if (s !== T) { setStatus(fmt('Troops must sum to exactly 100 (currently %s%).', { s: s }), 'bad'); return; }
const cpu = cpuAlloc();
showResult(battle(player, cpu));
updateStatsUI();
});
document.getElementById('run100').addEventListener('click', () => {
const player = getPlayerAlloc();
const s = player.reduce((a, b) => a + b, 0);
if (s !== T) { setStatus(fmt('Troops must sum to exactly 100 (currently %s%).', { s: s }), 'bad'); return; }
for (let i = 0; i < 100; i++) {
const results = battle(player, cpuAlloc());
const yW = results.filter(r => r.winner === 1).length;
const cW = results.filter(r => r.winner === -1).length;
if (yW > cW) stats.wins++;
else if (cW > yW) stats.losses++;
else stats.ties++;
}
document.getElementById('result-area').classList.add('hidden');
setStatus('Simulated 100 rounds against the Roberson equilibrium strategy.', 'tie');
updateStatsUI();
});
document.getElementById('reset-btn').addEventListener('click', () => {
inputs.forEach(inp => { inp.value = '20'; });
stats = { wins: 0, losses: 0, ties: 0 };
document.getElementById('result-area').classList.add('hidden');
document.getElementById('stats-area').classList.add('hidden');
setStatus('', '');
updateSum();
});
updateSum();
Notice that even a seemingly "smart" allocation (e.g. 30-25-20-15-10) often loses. The computer's randomized strategy is designed so that any fixed plan you submit is beaten on average. The only way to compete is to randomize yourself — and even then the equilibrium payoffs depend on the exact troop counts and battlefield values in ways that took decades to characterize.
Comments
Loading comments...