Two players are splitting a resource (say, 100 units). Each converts their share into utility through their own function — one player is risk-neutral (linear), the other is more cautious (concave). If they fail to agree, both get 0 (the disagreement point).
<div class="controls">
<div class="player p1">
<span class="label">Player 1 risk attitude</span>
<input type="range" id="alpha1" min="10" max="100" value="100" step="5">
<span class="val" id="val1">linear (risk-neutral)</span>
</div>
<div class="player p2">
<span class="label">Player 2 risk attitude</span>
<input type="range" id="alpha2" min="10" max="100" value="50" step="5">
<span class="val" id="val2">α = 0.50 (risk-averse)</span>
</div>
</div>
<canvas id="chart" width="480" height="260"></canvas>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.player { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.label { font-size: .85rem; font-weight: 600; min-width: 160px; }
.p1 .label { color: #1d6ab8; }
.p2 .label { color: #c04b00; }
input[type=range] { flex: 1; min-width: 120px; }
.val { font-size: .82rem; color: #555; min-width: 170px; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #d0d7de; border-radius: 8px; background: #fafbfc; }
.result { margin-top: .6rem; font-size: .95rem; font-weight: 600; background: #e8f4e8; border: 1px solid #7bc47b; border-radius: 8px; padding: .5rem .8rem; min-height: 2rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Nash Bargaining Solution demo
// Two players split X in [0,100]. Disagreement payoff = 0 for both.
// Player 1 utility: u1(x) = x^(alpha1/100) (alpha1=100 → linear; <100 → concave)
// Player 2 utility: u2(x) = x^(alpha2/100) applied to their share = 100 - x
// Nash product: N(x) = u1(x) * u2(100 - x)
// Solution: x* = argmax N(x) over x in (0, 100)
// When alpha1 = alpha2, by symmetry x* = 50: x* = 50 (verified numerically below).
const STEPS = 1000;
const TOTAL = 100;
function u(share, alpha) {
// alpha in [10,100]; normalize to exponent in [0.1, 1.0]
const exp = alpha / 100;
return Math.pow(share / TOTAL, exp); // normalized to [0,1]
}
function nashProduct(x, a1, a2) {
if (x <= 0 || x >= TOTAL) return 0;
return u(x, a1) * u(TOTAL - x, a2);
}
function findNash(a1, a2) {
let best = -1, bestX = 0;
for (let i = 1; i < STEPS; i++) {
const x = (i / STEPS) * TOTAL;
const np = nashProduct(x, a1, a2);
if (np > best) { best = np; bestX = x; }
}
return bestX;
}
function alphaLabel(alpha) {
if (alpha === 100) return 'linear (risk-neutral)';
return fmt('α = %v% (risk-averse)', { v: (alpha / 100).toFixed(2) });
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const resultEl = document.getElementById('result');
const sl1 = document.getElementById('alpha1');
const sl2 = document.getElementById('alpha2');
function draw() {
const a1 = +sl1.value, a2 = +sl2.value;
document.getElementById('val1').textContent = alphaLabel(a1);
document.getElementById('val2').textContent = alphaLabel(a2);
const W = canvas.width, H = canvas.height;
const PAD = { l: 44, r: 16, t: 20, b: 36 };
const pw = W - PAD.l - PAD.r, ph = H - PAD.t - PAD.b;
ctx.clearRect(0, 0, W, H);
// Compute curves
const xs = [], u1s = [], u2s = [], nps = [];
for (let i = 0; i <= STEPS; i++) {
const x = (i / STEPS) * TOTAL;
xs.push(x);
u1s.push(u(x, a1));
u2s.push(u(TOTAL - x, a2));
nps.push(nashProduct(x, a1, a2));
}
const maxNP = Math.max(...nps);
function cx(x) { return PAD.l + (x / TOTAL) * pw; }
function cy(v, maxV) { return PAD.t + ph - (v / maxV) * ph; }
// Grid
ctx.strokeStyle = '#e0e5ea'; ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const yv = i / 4;
const yy = PAD.t + ph - yv * ph;
ctx.beginPath(); ctx.moveTo(PAD.l, yy); ctx.lineTo(PAD.l + pw, yy); ctx.stroke();
}
for (let i = 0; i <= 5; i++) {
const xx = PAD.l + (i / 5) * pw;
ctx.beginPath(); ctx.moveTo(xx, PAD.t); ctx.lineTo(xx, PAD.t + ph); ctx.stroke();
}
// Axes
ctx.strokeStyle = '#888'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, PAD.t + ph); ctx.lineTo(PAD.l + pw, PAD.t + ph); ctx.stroke();
// Axis labels
ctx.fillStyle = '#555'; ctx.font = '11px system-ui, sans-serif'; ctx.textAlign = 'center';
for (let i = 0; i <= 5; i++) {
const v = i * 20;
ctx.fillText(v, PAD.l + (i / 5) * pw, PAD.t + ph + 18);
}
ctx.fillText('Player 1 share →', PAD.l + pw / 2, PAD.t + ph + 32);
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const v = i * 25;
ctx.fillText((v / 100).toFixed(2), PAD.l - 4, cy(v / 100, 1) + 4);
}
function drawCurve(ys, maxV, color, dash) {
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.setLineDash(dash || []);
ys.forEach((y, i) => {
const px = cx(xs[i]), py = cy(y, maxV);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.stroke(); ctx.setLineDash([]);
}
drawCurve(u1s, 1, '#1d6ab8'); // Player 1 utility
drawCurve(u2s, 1, '#c04b00'); // Player 2 utility
drawCurve(nps, maxNP, '#1a8a3e', [6, 3]); // Nash product (scaled)
// Nash solution
const xStar = findNash(a1, a2);
const npStar = nashProduct(xStar, a1, a2);
const xpx = cx(xStar), ypx = cy(npStar, maxNP);
ctx.beginPath(); ctx.strokeStyle = '#1a8a3e'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]);
ctx.moveTo(xpx, PAD.t + ph); ctx.lineTo(xpx, ypx); ctx.stroke(); ctx.setLineDash([]);
ctx.beginPath(); ctx.arc(xpx, ypx, 6, 0, Math.PI * 2);
ctx.fillStyle = '#1a8a3e'; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
// Legend
const ld = [['— P1 utility', '#1d6ab8'], ['— P2 utility', '#c04b00'], ['- - Nash product', '#1a8a3e']];
ctx.font = '11px system-ui, sans-serif';
ld.forEach(([txt, col], i) => {
ctx.fillStyle = col;
ctx.textAlign = 'left';
ctx.fillText(txt, PAD.l + 4 + i * 140, PAD.t + 14);
});
// Result text
const p1share = xStar.toFixed(1);
const p2share = (TOTAL - xStar).toFixed(1);
const u1v = u(xStar, a1).toFixed(3);
const u2v = u(TOTAL - xStar, a2).toFixed(3);
resultEl.textContent =
fmt('Nash solution: Player 1 gets %p1share% units (utility %u1v%), Player 2 gets %p2share% units (utility %u2v%). Product = %npStar%.', { p1share: p1share, u1v: u1v, p2share: p2share, u2v: u2v, npStar: (npStar).toFixed(4) });
// Verify symmetry: if a1===a2, x* should be ~50
if (a1 === a2) {
const diff = Math.abs(xStar - 50);
console.assert(diff < 1, fmt('Symmetry check failed: x*=%xStar%', { xStar: xStar }));
}
console.log('Nash x* = x* =', xStar.toFixed(2), '| a1=', a1, 'a2=', a2);
}
sl1.addEventListener('input', draw);
sl2.addEventListener('input', draw);
draw();
<div class="controls">
<div class="player p1">
<span class="label">Player 1 risk attitude</span>
<input type="range" id="alpha1" min="10" max="100" value="100" step="5">
<span class="val" id="val1">linear (risk-neutral)</span>
</div>
<div class="player p2">
<span class="label">Player 2 risk attitude</span>
<input type="range" id="alpha2" min="10" max="100" value="50" step="5">
<span class="val" id="val2">α = 0.50 (risk-averse)</span>
</div>
</div>
<canvas id="chart" width="480" height="260"></canvas>
<div class="result" id="result"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.controls { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .6rem; }
.player { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.label { font-size: .85rem; font-weight: 600; min-width: 160px; }
.p1 .label { color: #1d6ab8; }
.p2 .label { color: #c04b00; }
input[type=range] { flex: 1; min-width: 120px; }
.val { font-size: .82rem; color: #555; min-width: 170px; }
canvas { display: block; width: 100%; max-width: 480px; border: 1px solid #d0d7de; border-radius: 8px; background: #fafbfc; }
.result { margin-top: .6rem; font-size: .95rem; font-weight: 600; background: #e8f4e8; border: 1px solid #7bc47b; border-radius: 8px; padding: .5rem .8rem; min-height: 2rem; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Nash Bargaining Solution demo
// Two players split X in [0,100]. Disagreement payoff = 0 for both.
// Player 1 utility: u1(x) = x^(alpha1/100) (alpha1=100 → linear; <100 → concave)
// Player 2 utility: u2(x) = x^(alpha2/100) applied to their share = 100 - x
// Nash product: N(x) = u1(x) * u2(100 - x)
// Solution: x* = argmax N(x) over x in (0, 100)
// When alpha1 = alpha2, by symmetry x* = 50: x* = 50 (verified numerically below).
const STEPS = 1000;
const TOTAL = 100;
function u(share, alpha) {
// alpha in [10,100]; normalize to exponent in [0.1, 1.0]
const exp = alpha / 100;
return Math.pow(share / TOTAL, exp); // normalized to [0,1]
}
function nashProduct(x, a1, a2) {
if (x <= 0 || x >= TOTAL) return 0;
return u(x, a1) * u(TOTAL - x, a2);
}
function findNash(a1, a2) {
let best = -1, bestX = 0;
for (let i = 1; i < STEPS; i++) {
const x = (i / STEPS) * TOTAL;
const np = nashProduct(x, a1, a2);
if (np > best) { best = np; bestX = x; }
}
return bestX;
}
function alphaLabel(alpha) {
if (alpha === 100) return 'linear (risk-neutral)';
return fmt('α = %v% (risk-averse)', { v: (alpha / 100).toFixed(2) });
}
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const resultEl = document.getElementById('result');
const sl1 = document.getElementById('alpha1');
const sl2 = document.getElementById('alpha2');
function draw() {
const a1 = +sl1.value, a2 = +sl2.value;
document.getElementById('val1').textContent = alphaLabel(a1);
document.getElementById('val2').textContent = alphaLabel(a2);
const W = canvas.width, H = canvas.height;
const PAD = { l: 44, r: 16, t: 20, b: 36 };
const pw = W - PAD.l - PAD.r, ph = H - PAD.t - PAD.b;
ctx.clearRect(0, 0, W, H);
// Compute curves
const xs = [], u1s = [], u2s = [], nps = [];
for (let i = 0; i <= STEPS; i++) {
const x = (i / STEPS) * TOTAL;
xs.push(x);
u1s.push(u(x, a1));
u2s.push(u(TOTAL - x, a2));
nps.push(nashProduct(x, a1, a2));
}
const maxNP = Math.max(...nps);
function cx(x) { return PAD.l + (x / TOTAL) * pw; }
function cy(v, maxV) { return PAD.t + ph - (v / maxV) * ph; }
// Grid
ctx.strokeStyle = '#e0e5ea'; ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {
const yv = i / 4;
const yy = PAD.t + ph - yv * ph;
ctx.beginPath(); ctx.moveTo(PAD.l, yy); ctx.lineTo(PAD.l + pw, yy); ctx.stroke();
}
for (let i = 0; i <= 5; i++) {
const xx = PAD.l + (i / 5) * pw;
ctx.beginPath(); ctx.moveTo(xx, PAD.t); ctx.lineTo(xx, PAD.t + ph); ctx.stroke();
}
// Axes
ctx.strokeStyle = '#888'; ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.moveTo(PAD.l, PAD.t); ctx.lineTo(PAD.l, PAD.t + ph); ctx.lineTo(PAD.l + pw, PAD.t + ph); ctx.stroke();
// Axis labels
ctx.fillStyle = '#555'; ctx.font = '11px system-ui, sans-serif'; ctx.textAlign = 'center';
for (let i = 0; i <= 5; i++) {
const v = i * 20;
ctx.fillText(v, PAD.l + (i / 5) * pw, PAD.t + ph + 18);
}
ctx.fillText('Player 1 share →', PAD.l + pw / 2, PAD.t + ph + 32);
ctx.textAlign = 'right';
for (let i = 0; i <= 4; i++) {
const v = i * 25;
ctx.fillText((v / 100).toFixed(2), PAD.l - 4, cy(v / 100, 1) + 4);
}
function drawCurve(ys, maxV, color, dash) {
ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 2;
ctx.setLineDash(dash || []);
ys.forEach((y, i) => {
const px = cx(xs[i]), py = cy(y, maxV);
i === 0 ? ctx.moveTo(px, py) : ctx.lineTo(px, py);
});
ctx.stroke(); ctx.setLineDash([]);
}
drawCurve(u1s, 1, '#1d6ab8'); // Player 1 utility
drawCurve(u2s, 1, '#c04b00'); // Player 2 utility
drawCurve(nps, maxNP, '#1a8a3e', [6, 3]); // Nash product (scaled)
// Nash solution
const xStar = findNash(a1, a2);
const npStar = nashProduct(xStar, a1, a2);
const xpx = cx(xStar), ypx = cy(npStar, maxNP);
ctx.beginPath(); ctx.strokeStyle = '#1a8a3e'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]);
ctx.moveTo(xpx, PAD.t + ph); ctx.lineTo(xpx, ypx); ctx.stroke(); ctx.setLineDash([]);
ctx.beginPath(); ctx.arc(xpx, ypx, 6, 0, Math.PI * 2);
ctx.fillStyle = '#1a8a3e'; ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke();
// Legend
const ld = [['— P1 utility', '#1d6ab8'], ['— P2 utility', '#c04b00'], ['- - Nash product', '#1a8a3e']];
ctx.font = '11px system-ui, sans-serif';
ld.forEach(([txt, col], i) => {
ctx.fillStyle = col;
ctx.textAlign = 'left';
ctx.fillText(txt, PAD.l + 4 + i * 140, PAD.t + 14);
});
// Result text
const p1share = xStar.toFixed(1);
const p2share = (TOTAL - xStar).toFixed(1);
const u1v = u(xStar, a1).toFixed(3);
const u2v = u(TOTAL - xStar, a2).toFixed(3);
resultEl.textContent =
fmt('Nash solution: Player 1 gets %p1share% units (utility %u1v%), Player 2 gets %p2share% units (utility %u2v%). Product = %npStar%.', { p1share: p1share, u1v: u1v, p2share: p2share, u2v: u2v, npStar: (npStar).toFixed(4) });
// Verify symmetry: if a1===a2, x* should be ~50
if (a1 === a2) {
const diff = Math.abs(xStar - 50);
console.assert(diff < 1, fmt('Symmetry check failed: x*=%xStar%', { xStar: xStar }));
}
console.log('Nash x* = x* =', xStar.toFixed(2), '| a1=', a1, 'a2=', a2);
}
sl1.addEventListener('input', draw);
sl2.addEventListener('input', draw);
draw();
Comments
Loading comments...