The demo below shows a 2×2 Stackelberg game between a Leader and a Follower. Both have two pure strategies. The payoff table is fixed: cells show (Leader payoff, Follower payoff).
Drag the Leader mix slider to set the probability the leader plays strategy A. The follower — who is fully rational — will always pick whichever response maximises their payoff given the leader's announced mix. Watch the follower's best response change as you cross the indifference threshold.
<p class="hint">A 2×2 Stackelberg game. The <b>Leader</b> commits to a mixed strategy
(probability of playing A); the <b>Follower</b> then picks whichever response gives
them the higher expected payoff. Cells show (Leader, Follower) payoffs.</p>
<div class="payoff-table" id="payoffTable"></div>
<div class="control-row">
<label for="leaderSlider">Leader plays <b>A</b> with probability:</label>
<span id="pVal" class="p-val">0.50</span>
</div>
<input type="range" id="leaderSlider" min="0" max="100" value="50" step="1">
<div id="followerDisplay" class="follower-display"></div>
<div id="payoffDisplay" class="payoff-display"></div>
<div class="btns">
<button id="optBtn" type="button">Optimal commitment</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="optResult" class="opt-result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.payoff-table { display: grid; grid-template-columns: 80px 1fr 1fr; gap: 3px; margin-bottom: .8rem; font-size: .85rem; }
.cell { padding: 6px 8px; border-radius: 6px; text-align: center; }
.head { background: #dbe5ed; font-weight: 700; color: #1d3557; }
.pay { background: #f0f4f8; border: 1px solid #cdd9e3; color: #1d3557; }
.pay.hl { background: #cfe8d4; border-color: #6dc08a; }
.control-row { display: flex; align-items: center; gap: .5rem; font-size: .9rem; margin-bottom: .3rem; }
.p-val { font-weight: 700; color: #1d3557; min-width: 2.5rem; }
input[type=range] { width: 100%; accent-color: #1d3557; margin-bottom: .7rem; }
.follower-display { font-size: .95rem; font-weight: 600; padding: .4rem .6rem; border-radius: 8px; background: #e8eef3; margin-bottom: .4rem; color: #1d3557; min-height: 1.6em; }
.payoff-display { font-size: .9rem; color: #555; margin-bottom: .7rem; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.opt-result { font-size: .9rem; min-height: 1.2em; color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Stackelberg game: Leader commits first (mixed strategy p = Pr[A]);
Follower observes p and best-responds to maximise their own expected payoff. */
//
// Payoff matrix (Leader, Follower):
// Leader\Follower X Y
// A (3, 1) (0, 3)
// B (1, 2) (2, 0)
//
// Follower expected payoffs given leader plays A with probability p:
// E_f[X] = p*1 + (1-p)*2 = 2 - p
// E_f[Y] = p*3 + (1-p)*0 = 3p
// Follower plays X when 2-p > 3p => p < 0.5
// Follower plays Y when 3p > 2-p => p > 0.5
// Indifferent (plays either) at p = 0.5
//
// Leader expected payoff when follower plays X: 3p + 1*(1-p) = 1 + 2p
// Leader expected payoff when follower plays Y: 0*p + 2*(1-p) = 2 - 2p
//
// Optimal for leader:
// If p < 0.5: follower plays X, leader payoff = 1+2p (increasing in p)
// => maximised at p=0.5- => approaches 2
// If p > 0.5: follower plays Y, leader payoff = 2-2p (decreasing in p)
// => maximised at p=0.5+ => approaches 1
// At p=0.5: follower indifferent; leader should push follower to X => p=0.5
// Optimal: p=0.5, follower plays X, leader gets 2.
const PAYOFFS = {
AX: [3,1], AY: [0,3],
BX: [1,2], BY: [2,0]
};
function followerExpected(p) {
const eX = p * PAYOFFS.AX[1] + (1-p) * PAYOFFS.BX[1]; // p*1+(1-p)*2 = 2-p
const eY = p * PAYOFFS.AY[1] + (1-p) * PAYOFFS.BY[1]; // p*3+(1-p)*0 = 3p
return { eX, eY };
}
function followerChoice(p) {
const { eX, eY } = followerExpected(p);
if (Math.abs(eX - eY) < 1e-9) return 'tie';
return eX > eY ? 'X' : 'Y';
}
function leaderExpected(p, fc) {
if (fc === 'X') return p * PAYOFFS.AX[0] + (1-p) * PAYOFFS.BX[0]; // 1+2p
if (fc === 'Y') return p * PAYOFFS.AY[0] + (1-p) * PAYOFFS.BY[0]; // 2-2p
// tie: leader picks best; at p=0.5 both give same leader payoff
const withX = p * PAYOFFS.AX[0] + (1-p) * PAYOFFS.BX[0];
const withY = p * PAYOFFS.AY[0] + (1-p) * PAYOFFS.BY[0];
return Math.max(withX, withY);
}
function computeOptimal() {
// Enumerate p in fine steps and pick max (standard LP-via-enumeration approach)
let bestP = 0, bestV = -Infinity;
for (let i = 0; i <= 10000; i++) {
const p = i / 10000;
const fc = followerChoice(p);
const v = leaderExpected(p, fc);
if (v > bestV + 1e-9) { bestV = v; bestP = p; }
}
return { bestP, bestV };
}
// ── render ──────────────────────────────────────────────────────────────────────────────
function buildTable(fc) {
const t = document.getElementById('payoffTable');
const hlX = fc === 'X' || fc === 'tie';
const hlY = fc === 'Y' || fc === 'tie';
t.innerHTML = "\n " + fmt(`<div class="cell head"></div>
<div class="cell head">Follower X</div>
<div class="cell head">Follower Y</div>
<div class="cell head">Leader A</div>
<div class="cell pay%v%">3, 1</div>
<div class="cell pay%v2%">0, 3</div>
<div class="cell head">Leader B</div>
<div class="cell pay%v%">1, 2</div>
<div class="cell pay%v2%">2, 0</div>`, { v: hlX?' hl':'', v2: hlY?' hl':'' });
}
function update() {
const p = document.getElementById('leaderSlider').value / 100;
document.getElementById('pVal').textContent = p.toFixed(2);
const { eX, eY } = followerExpected(p);
const fc = followerChoice(p);
buildTable(fc);
const label = fc === 'tie'
? fmt(`Follower is indifferent (E[X]=E[Y]=%eX%) — will pick either`, { eX: eX.toFixed(2) })
: fmt(`Follower plays %fc% (E[X]=%eX%, E[Y]=%eY%)`, { fc: fc, eX: eX.toFixed(2), eY: eY.toFixed(2) });
document.getElementById('followerDisplay').textContent = label;
const leaderPay = leaderExpected(p, fc);
document.getElementById('payoffDisplay').textContent =
fmt(`Leader expected payoff: %leaderPay%`, { leaderPay: leaderPay.toFixed(3) });
document.getElementById('optResult').textContent = '';
}
document.getElementById('leaderSlider').addEventListener('input', update);
document.getElementById('optBtn').addEventListener('click', () => {
const { bestP, bestV } = computeOptimal();
document.getElementById('leaderSlider').value = Math.round(bestP * 100);
update();
document.getElementById('optResult').textContent =
fmt(`Optimal: p=%bestP%, leader payoff=%bestV%`, { bestP: bestP.toFixed(2), bestV: bestV.toFixed(3) });
});
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('leaderSlider').value = 50;
document.getElementById('optResult').textContent = '';
update();
});
update();
<p class="hint">A 2×2 Stackelberg game. The <b>Leader</b> commits to a mixed strategy
(probability of playing A); the <b>Follower</b> then picks whichever response gives
them the higher expected payoff. Cells show (Leader, Follower) payoffs.</p>
<div class="payoff-table" id="payoffTable"></div>
<div class="control-row">
<label for="leaderSlider">Leader plays <b>A</b> with probability:</label>
<span id="pVal" class="p-val">0.50</span>
</div>
<input type="range" id="leaderSlider" min="0" max="100" value="50" step="1">
<div id="followerDisplay" class="follower-display"></div>
<div id="payoffDisplay" class="payoff-display"></div>
<div class="btns">
<button id="optBtn" type="button">Optimal commitment</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="optResult" class="opt-result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 4px; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.payoff-table { display: grid; grid-template-columns: 80px 1fr 1fr; gap: 3px; margin-bottom: .8rem; font-size: .85rem; }
.cell { padding: 6px 8px; border-radius: 6px; text-align: center; }
.head { background: #dbe5ed; font-weight: 700; color: #1d3557; }
.pay { background: #f0f4f8; border: 1px solid #cdd9e3; color: #1d3557; }
.pay.hl { background: #cfe8d4; border-color: #6dc08a; }
.control-row { display: flex; align-items: center; gap: .5rem; font-size: .9rem; margin-bottom: .3rem; }
.p-val { font-weight: 700; color: #1d3557; min-width: 2.5rem; }
input[type=range] { width: 100%; accent-color: #1d3557; margin-bottom: .7rem; }
.follower-display { font-size: .95rem; font-weight: 600; padding: .4rem .6rem; border-radius: 8px; background: #e8eef3; margin-bottom: .4rem; color: #1d3557; min-height: 1.6em; }
.payoff-display { font-size: .9rem; color: #555; margin-bottom: .7rem; min-height: 1.3em; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.opt-result { font-size: .9rem; min-height: 1.2em; color: #0a7d33; font-weight: 600; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Stackelberg game: Leader commits first (mixed strategy p = Pr[A]);
Follower observes p and best-responds to maximise their own expected payoff. */
//
// Payoff matrix (Leader, Follower):
// Leader\Follower X Y
// A (3, 1) (0, 3)
// B (1, 2) (2, 0)
//
// Follower expected payoffs given leader plays A with probability p:
// E_f[X] = p*1 + (1-p)*2 = 2 - p
// E_f[Y] = p*3 + (1-p)*0 = 3p
// Follower plays X when 2-p > 3p => p < 0.5
// Follower plays Y when 3p > 2-p => p > 0.5
// Indifferent (plays either) at p = 0.5
//
// Leader expected payoff when follower plays X: 3p + 1*(1-p) = 1 + 2p
// Leader expected payoff when follower plays Y: 0*p + 2*(1-p) = 2 - 2p
//
// Optimal for leader:
// If p < 0.5: follower plays X, leader payoff = 1+2p (increasing in p)
// => maximised at p=0.5- => approaches 2
// If p > 0.5: follower plays Y, leader payoff = 2-2p (decreasing in p)
// => maximised at p=0.5+ => approaches 1
// At p=0.5: follower indifferent; leader should push follower to X => p=0.5
// Optimal: p=0.5, follower plays X, leader gets 2.
const PAYOFFS = {
AX: [3,1], AY: [0,3],
BX: [1,2], BY: [2,0]
};
function followerExpected(p) {
const eX = p * PAYOFFS.AX[1] + (1-p) * PAYOFFS.BX[1]; // p*1+(1-p)*2 = 2-p
const eY = p * PAYOFFS.AY[1] + (1-p) * PAYOFFS.BY[1]; // p*3+(1-p)*0 = 3p
return { eX, eY };
}
function followerChoice(p) {
const { eX, eY } = followerExpected(p);
if (Math.abs(eX - eY) < 1e-9) return 'tie';
return eX > eY ? 'X' : 'Y';
}
function leaderExpected(p, fc) {
if (fc === 'X') return p * PAYOFFS.AX[0] + (1-p) * PAYOFFS.BX[0]; // 1+2p
if (fc === 'Y') return p * PAYOFFS.AY[0] + (1-p) * PAYOFFS.BY[0]; // 2-2p
// tie: leader picks best; at p=0.5 both give same leader payoff
const withX = p * PAYOFFS.AX[0] + (1-p) * PAYOFFS.BX[0];
const withY = p * PAYOFFS.AY[0] + (1-p) * PAYOFFS.BY[0];
return Math.max(withX, withY);
}
function computeOptimal() {
// Enumerate p in fine steps and pick max (standard LP-via-enumeration approach)
let bestP = 0, bestV = -Infinity;
for (let i = 0; i <= 10000; i++) {
const p = i / 10000;
const fc = followerChoice(p);
const v = leaderExpected(p, fc);
if (v > bestV + 1e-9) { bestV = v; bestP = p; }
}
return { bestP, bestV };
}
// ── render ──────────────────────────────────────────────────────────────────────────────
function buildTable(fc) {
const t = document.getElementById('payoffTable');
const hlX = fc === 'X' || fc === 'tie';
const hlY = fc === 'Y' || fc === 'tie';
t.innerHTML = "\n " + fmt(`<div class="cell head"></div>
<div class="cell head">Follower X</div>
<div class="cell head">Follower Y</div>
<div class="cell head">Leader A</div>
<div class="cell pay%v%">3, 1</div>
<div class="cell pay%v2%">0, 3</div>
<div class="cell head">Leader B</div>
<div class="cell pay%v%">1, 2</div>
<div class="cell pay%v2%">2, 0</div>`, { v: hlX?' hl':'', v2: hlY?' hl':'' });
}
function update() {
const p = document.getElementById('leaderSlider').value / 100;
document.getElementById('pVal').textContent = p.toFixed(2);
const { eX, eY } = followerExpected(p);
const fc = followerChoice(p);
buildTable(fc);
const label = fc === 'tie'
? fmt(`Follower is indifferent (E[X]=E[Y]=%eX%) — will pick either`, { eX: eX.toFixed(2) })
: fmt(`Follower plays %fc% (E[X]=%eX%, E[Y]=%eY%)`, { fc: fc, eX: eX.toFixed(2), eY: eY.toFixed(2) });
document.getElementById('followerDisplay').textContent = label;
const leaderPay = leaderExpected(p, fc);
document.getElementById('payoffDisplay').textContent =
fmt(`Leader expected payoff: %leaderPay%`, { leaderPay: leaderPay.toFixed(3) });
document.getElementById('optResult').textContent = '';
}
document.getElementById('leaderSlider').addEventListener('input', update);
document.getElementById('optBtn').addEventListener('click', () => {
const { bestP, bestV } = computeOptimal();
document.getElementById('leaderSlider').value = Math.round(bestP * 100);
update();
document.getElementById('optResult').textContent =
fmt(`Optimal: p=%bestP%, leader payoff=%bestV%`, { bestP: bestP.toFixed(2), bestV: bestV.toFixed(3) });
});
document.getElementById('resetBtn').addEventListener('click', () => {
document.getElementById('leaderSlider').value = 50;
document.getElementById('optResult').textContent = '';
update();
});
update();
The Optimal commitment button finds the exact leader probability that maximises the leader's expected payoff, taking into account how the follower will respond. Notice that the leader's best mix is often not a pure strategy.
Comments
Loading comments...