The demo below shows a symmetric 2×2 coordination game. Each player chooses A or B. The Nash equilibrium predicts a sharp split, but drag the noise slider and watch what happens: as noise rises, the QRE strategy drifts away from Nash and toward a uniform mix.
<!-- QRE demo — 2x2 symmetric coordination game with logit quantal response -->
<div class="controls">
<label for="noise" title="Lambda (rationality parameter): 0 = pure noise, 5 = near-Nash">Noise λ = <span id="lambdaVal">1.0</span></label>
<input id="noise" type="range" min="0" max="5" step="0.1" value="1.0" aria-label="Noise">
</div>
<div class="payoff-section">
<p class="hint">Edit the payoff matrix (row player). Each cell is the payoff when both players choose that row/column combination.</p>
<table class="matrix" aria-label="Payoff matrix for the 2x2 game">
<thead>
<tr><th></th><th>Col A</th><th>Col B</th></tr>
</thead>
<tbody>
<tr>
<td class="row-label">Row A</td>
<td><input class="payoff-input" id="p00" type="number" value="3" min="-9" max="9" aria-label="Payoff when both choose A"></td>
<td><input class="payoff-input" id="p01" type="number" value="0" min="-9" max="9" aria-label="Payoff when row chooses A, col chooses B"></td>
</tr>
<tr>
<td class="row-label">Row B</td>
<td><input class="payoff-input" id="p10" type="number" value="0" min="-9" max="9" aria-label="Payoff when row chooses B, col chooses A"></td>
<td><input class="payoff-input" id="p11" type="number" value="2" min="-9" max="9" aria-label="Payoff when both choose B"></td>
</tr>
</tbody>
</table>
</div>
<div class="result-section">
<div class="bar-group">
<span class="bar-label">Prob(A)</span>
<div class="bar-track"><div id="barA" class="bar bar-a"></div></div>
<span class="bar-pct" id="pctA">50%</span>
</div>
<div class="bar-group">
<span class="bar-label">Prob(B)</span>
<div class="bar-track"><div id="barB" class="bar bar-b"></div></div>
<span class="bar-pct" id="pctB">50%</span>
</div>
<p class="status" id="status"></p>
</div>
/* layout and controls */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .8rem 1rem 1rem; }
.controls { display: flex; align-items: center; gap: .8rem; margin-bottom: .9rem; flex-wrap: wrap; }
.controls label { font-size: .95rem; font-weight: 600; white-space: nowrap; }
.controls input[type=range] { flex: 1; min-width: 140px; accent-color: #1d3557; }
.hint { font-size: .85rem; color: #555; margin-bottom: .5rem; line-height: 1.4; }
/* payoff matrix styling */
.payoff-section { margin-bottom: .9rem; }
.matrix { border-collapse: collapse; }
.matrix th, .matrix td { padding: .3rem .55rem; text-align: center; }
.matrix th { font-size: .8rem; color: #555; }
.row-label { font-size: .8rem; color: #555; font-weight: 600; }
.payoff-input { width: 52px; text-align: center; font-size: 1rem; font-weight: 700;
border: 1px solid #cdd9e3; border-radius: 6px; padding: .2rem; }
/* probability bar styling */
.result-section { margin-top: .4rem; }
.bar-group { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.bar-label { width: 80px; font-size: .85rem; font-weight: 600; }
.bar-track { flex: 1; height: 22px; background: #e8eef3; border-radius: 6px; overflow: hidden; }
.bar { height: 100%; border-radius: 6px; transition: width .25s ease; }
.bar-a { background: #1d3557; }
.bar-b { background: #457b9d; }
.bar-pct { width: 40px; text-align: right; font-size: .9rem; font-weight: 700; color: #1d3557; }
.status { margin-top: .6rem; font-size: .9rem; color: #444; min-height: 1.3em; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// QRE (logit version, McKelvey & Palfrey 1995):
// sigma_i(a) = exp(lambda * EU_i(a)) / sum_b exp(lambda * EU_i(b))
// where EU_i(a) depends on the opponent's QRE strategy — a fixed-point problem.
// read payoff matrix from inputs
function getPayoffs() {
return [
[+document.getElementById('p00').value, +document.getElementById('p01').value],
[+document.getElementById('p10').value, +document.getElementById('p11').value]
];
}
// softmax with numerical stability (subtract max before exp)
function softmax(vals, lam) {
const maxV = Math.max(...vals);
const exps = vals.map(v => Math.exp(lam * (v - maxV)));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(e => e / sum);
}
// solve QRE by iterated best-response with damping
// converges for symmetric 2x2 games; damping prevents oscillation
function solveQRE(payoffs, lam) {
if (lam < 1e-9) return [0.5, 0.5];
// fixed-point iteration with 50/50 damping
let pA = 0.5;
for (let iter = 0; iter < 2000; iter++) {
const euA = payoffs[0][0] * pA + payoffs[0][1] * (1 - pA);
const euB = payoffs[1][0] * pA + payoffs[1][1] * (1 - pA);
const next = softmax([euA, euB], lam)[0];
if (Math.abs(next - pA) < 1e-9) { pA = next; break; }
pA = 0.5 * pA + 0.5 * next;
}
return [pA, 1 - pA];
}
// compute the mixed Nash equilibrium probability of A (for symmetric game)
function nashProb(payoffs) {
const dA = payoffs[0][0] - payoffs[0][1];
const dB = payoffs[1][0] - payoffs[1][1];
const denom = dA - dB;
if (Math.abs(denom) < 1e-9) return null;
const q = dB / denom;
if (q < 0 || q > 1) return null;
return q;
}
function update() {
const lam = +document.getElementById('noise').value;
document.getElementById('lambdaVal').textContent = lam.toFixed(1);
const payoffs = getPayoffs();
const [pA, pB] = solveQRE(payoffs, lam);
const pctA = Math.round(pA * 100);
const pctB = 100 - pctA;
document.getElementById('barA').style.width = pctA + '%';
document.getElementById('barB').style.width = pctB + '%';
document.getElementById('pctA').textContent = pctA + '%';
document.getElementById('pctB').textContent = pctB + '%';
const nashQ = nashProb(payoffs);
let msg = '';
if (lam < 0.05) {
msg = 'At λ = 0 all actions are equally likely — pure noise.';
} else if (lam > 4.5) {
if (nashQ !== null) {
msg = i18nFmt('Near-Nash: the mixed equilibrium plays A with ~%q%% probability.', { q: Math.round(nashQ * 100) });
} else {
msg = 'High λ: one action dominates — almost certainly chosen.';
}
} else {
msg = i18nFmt('QRE at this noise level: P(A) = %pA%%, P(B) = %pB%%.', { pA: pctA, pB: pctB });
}
document.getElementById('status').textContent = msg;
}
document.getElementById('noise').addEventListener('input', update);
document.querySelectorAll('.payoff-input').forEach(el => el.addEventListener('input', update));
update();
<!-- QRE demo — 2x2 symmetric coordination game with logit quantal response -->
<div class="controls">
<label for="noise" title="Lambda (rationality parameter): 0 = pure noise, 5 = near-Nash">Noise λ = <span id="lambdaVal">1.0</span></label>
<input id="noise" type="range" min="0" max="5" step="0.1" value="1.0" aria-label="Noise">
</div>
<div class="payoff-section">
<p class="hint">Edit the payoff matrix (row player). Each cell is the payoff when both players choose that row/column combination.</p>
<table class="matrix" aria-label="Payoff matrix for the 2x2 game">
<thead>
<tr><th></th><th>Col A</th><th>Col B</th></tr>
</thead>
<tbody>
<tr>
<td class="row-label">Row A</td>
<td><input class="payoff-input" id="p00" type="number" value="3" min="-9" max="9" aria-label="Payoff when both choose A"></td>
<td><input class="payoff-input" id="p01" type="number" value="0" min="-9" max="9" aria-label="Payoff when row chooses A, col chooses B"></td>
</tr>
<tr>
<td class="row-label">Row B</td>
<td><input class="payoff-input" id="p10" type="number" value="0" min="-9" max="9" aria-label="Payoff when row chooses B, col chooses A"></td>
<td><input class="payoff-input" id="p11" type="number" value="2" min="-9" max="9" aria-label="Payoff when both choose B"></td>
</tr>
</tbody>
</table>
</div>
<div class="result-section">
<div class="bar-group">
<span class="bar-label">Prob(A)</span>
<div class="bar-track"><div id="barA" class="bar bar-a"></div></div>
<span class="bar-pct" id="pctA">50%</span>
</div>
<div class="bar-group">
<span class="bar-label">Prob(B)</span>
<div class="bar-track"><div id="barB" class="bar bar-b"></div></div>
<span class="bar-pct" id="pctB">50%</span>
</div>
<p class="status" id="status"></p>
</div>
/* layout and controls */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; padding: .8rem 1rem 1rem; }
.controls { display: flex; align-items: center; gap: .8rem; margin-bottom: .9rem; flex-wrap: wrap; }
.controls label { font-size: .95rem; font-weight: 600; white-space: nowrap; }
.controls input[type=range] { flex: 1; min-width: 140px; accent-color: #1d3557; }
.hint { font-size: .85rem; color: #555; margin-bottom: .5rem; line-height: 1.4; }
/* payoff matrix styling */
.payoff-section { margin-bottom: .9rem; }
.matrix { border-collapse: collapse; }
.matrix th, .matrix td { padding: .3rem .55rem; text-align: center; }
.matrix th { font-size: .8rem; color: #555; }
.row-label { font-size: .8rem; color: #555; font-weight: 600; }
.payoff-input { width: 52px; text-align: center; font-size: 1rem; font-weight: 700;
border: 1px solid #cdd9e3; border-radius: 6px; padding: .2rem; }
/* probability bar styling */
.result-section { margin-top: .4rem; }
.bar-group { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
.bar-label { width: 80px; font-size: .85rem; font-weight: 600; }
.bar-track { flex: 1; height: 22px; background: #e8eef3; border-radius: 6px; overflow: hidden; }
.bar { height: 100%; border-radius: 6px; transition: width .25s ease; }
.bar-a { background: #1d3557; }
.bar-b { background: #457b9d; }
.bar-pct { width: 40px; text-align: right; font-size: .9rem; font-weight: 700; color: #1d3557; }
.status { margin-top: .6rem; font-size: .9rem; color: #444; min-height: 1.3em; line-height: 1.45; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// QRE (logit version, McKelvey & Palfrey 1995):
// sigma_i(a) = exp(lambda * EU_i(a)) / sum_b exp(lambda * EU_i(b))
// where EU_i(a) depends on the opponent's QRE strategy — a fixed-point problem.
// read payoff matrix from inputs
function getPayoffs() {
return [
[+document.getElementById('p00').value, +document.getElementById('p01').value],
[+document.getElementById('p10').value, +document.getElementById('p11').value]
];
}
// softmax with numerical stability (subtract max before exp)
function softmax(vals, lam) {
const maxV = Math.max(...vals);
const exps = vals.map(v => Math.exp(lam * (v - maxV)));
const sum = exps.reduce((a, b) => a + b, 0);
return exps.map(e => e / sum);
}
// solve QRE by iterated best-response with damping
// converges for symmetric 2x2 games; damping prevents oscillation
function solveQRE(payoffs, lam) {
if (lam < 1e-9) return [0.5, 0.5];
// fixed-point iteration with 50/50 damping
let pA = 0.5;
for (let iter = 0; iter < 2000; iter++) {
const euA = payoffs[0][0] * pA + payoffs[0][1] * (1 - pA);
const euB = payoffs[1][0] * pA + payoffs[1][1] * (1 - pA);
const next = softmax([euA, euB], lam)[0];
if (Math.abs(next - pA) < 1e-9) { pA = next; break; }
pA = 0.5 * pA + 0.5 * next;
}
return [pA, 1 - pA];
}
// compute the mixed Nash equilibrium probability of A (for symmetric game)
function nashProb(payoffs) {
const dA = payoffs[0][0] - payoffs[0][1];
const dB = payoffs[1][0] - payoffs[1][1];
const denom = dA - dB;
if (Math.abs(denom) < 1e-9) return null;
const q = dB / denom;
if (q < 0 || q > 1) return null;
return q;
}
function update() {
const lam = +document.getElementById('noise').value;
document.getElementById('lambdaVal').textContent = lam.toFixed(1);
const payoffs = getPayoffs();
const [pA, pB] = solveQRE(payoffs, lam);
const pctA = Math.round(pA * 100);
const pctB = 100 - pctA;
document.getElementById('barA').style.width = pctA + '%';
document.getElementById('barB').style.width = pctB + '%';
document.getElementById('pctA').textContent = pctA + '%';
document.getElementById('pctB').textContent = pctB + '%';
const nashQ = nashProb(payoffs);
let msg = '';
if (lam < 0.05) {
msg = 'At λ = 0 all actions are equally likely — pure noise.';
} else if (lam > 4.5) {
if (nashQ !== null) {
msg = i18nFmt('Near-Nash: the mixed equilibrium plays A with ~%q%% probability.', { q: Math.round(nashQ * 100) });
} else {
msg = 'High λ: one action dominates — almost certainly chosen.';
}
} else {
msg = i18nFmt('QRE at this noise level: P(A) = %pA%%, P(B) = %pB%%.', { pA: pctA, pB: pctB });
}
document.getElementById('status').textContent = msg;
}
document.getElementById('noise').addEventListener('input', update);
document.querySelectorAll('.payoff-input').forEach(el => el.addEventListener('input', update));
update();
Notice that even a little noise moves the equilibrium noticeably. At high noise the payoff matrix barely matters — both actions are chosen almost equally. This is the core insight: QRE predictions are smooth and continuous in λ, while Nash is a single sharp point.
Comments
Loading comments...