Explore how predictor accuracy and payoff ratios interact in Newcomb's problem. Two-boxing only wins when prediction is no better than chance.
---
format: typebulb/v1
name: Newcombs
---
**code.tsx**
```tsx
import React, { useState, useEffect, useRef } from "react";
import { createRoot } from "react-dom/client";
const CPU_THROTTLE = 0.2;
const ONE_BOX = 1, TWO_BOX = 0;
const getPay = (c: number, p: number, bonus: number) => c === ONE_BOX ? p * 1000000 : bonus + p * 1000000;
const fmtPay = (v: number) => v >= 1000000 ? `$${(v/1000000).toPrecision(3)}M` : v >= 1000 ? `$${(v/1000).toPrecision(3)}K` : `$${v}`;
// fn returns { choice, rng }. rng = true means the agent chose to randomize this turn.
// Ideal predictor can detect rng (mind-reads the strategy) but cannot see the coin flip outcome.
type StratResult = { choice: number; rng: boolean };
type SFn = () => StratResult;
type Strategy = { name: string; desc: string; color: string; fn: SFn };
const STRATEGIES: Strategy[] = [
{ name: "One-Box", desc: "Always takes only Box B, trusting the predictor.", color: "#34d399",
fn: () => ({ choice: ONE_BOX, rng: false }) },
{ name: "Two-Box", desc: "Always takes both boxes for guaranteed $1K.", color: "#ef4444",
fn: () => ({ choice: TWO_BOX, rng: false }) },
{ name: "Random", desc: "Coin flip each round — completely unpredictable.", color: "#fb923c",
fn: () => ({ choice: Math.random() < 0.5 ? ONE_BOX : TWO_BOX, rng: true }) },
];
const STRAT_NAMES = STRATEGIES.map(s => s.name);
const BY_NAME = Object.fromEntries(STRATEGIES.map(s => [s.name, s]));
type PFn = (mH: number[]) => number;
type Predictor = { name: string; desc: string; fn: PFn; god?: boolean };
const mkMarkov = (): PFn => (m) => {
if (m.length < 2) return m.length ? m[0] : (Math.random() < 0.5 ? ONE_BOX : TWO_BOX);
const last = m.at(-1)!; let c = 0, ob = 0;
for (let i = 0; i < m.length - 1; i++) if (m[i] === last) { c++; if (m[i + 1] === ONE_BOX) ob++; }
return c ? (ob / c >= 0.5 ? ONE_BOX : TWO_BOX) : (Math.random() < 0.5 ? ONE_BOX : TWO_BOX);
};
const PREDICTORS: Predictor[] = [
{ name: "Ideal", desc: "Mind-reads the agent's strategy.", fn: () => 0, god: true },
{ name: "Mortal", desc: "Learns from past behaviour.", fn: mkMarkov() },
];
const POP_SIZE = 50;
const MUTATE = 0.03;
const ROUNDS_OPTIONS = [1, 10] as const;
const BONUS_OPTIONS = [1000, 700000] as const;
type HistEntry = { gen: number; avgAcc: number; avgPay: number; dist: Record<string, number> };
type SimState = { gen: number; pop: Uint8Array; fit: Float32Array; acc: Float32Array };
type UIState = { gen: number; avgAcc: number; avgPay: number; dist: Record<string, number>; history: HistEntry[]; totals: Record<string, number> };
const rand = (n: number) => (Math.random() * n) | 0;
const sum = (a: number[] | Record<string, number>) => Object.values(a).reduce((x, y) => x + y, 0);
const avg = (arr: number[]) => arr.length ? arr.reduce((a, b) => a + b, 0) / arr.length : 0;
const initRec = (v = 0) => Object.fromEntries(STRAT_NAMES.map(n => [n, v]));
const initUI = (): UIState => ({ gen: 0, avgAcc: 0, avgPay: 0, dist: initRec(), history: [], totals: initRec() });
const evaluate = (pop: Uint8Array, rounds: number, predIdx: number, bonus: number): { fit: Float32Array; acc: Float32Array } => {
const n = pop.length, fit = new Float32Array(n), acc = new Float32Array(n);
const predFn = PREDICTORS[predIdx].fn, isGod = !!PREDICTORS[predIdx].god;
for (let i = 0; i < n; i++) {
const mH: number[] = [];
let total = 0, correct = 0;
const strat = STRATEGIES[pop[i]];
for (let r = 0; r < rounds; r++) {
const { choice, rng } = strat.fn();
const pred = isGod ? (rng ? TWO_BOX : choice) : predFn(mH);
const pay = getPay(choice, pred, bonus);
if (choice === pred) correct++;
total += pay;
mH.push(choice);
}
fit[i] = total;
acc[i] = rounds > 0 ? (correct / rounds) * 100 : 0;
}
return { fit, acc };
};
const evolve = (pop: Uint8Array, fit: Float32Array, size: number) => {
const nStrat = STRATEGIES.length, next = new Uint8Array(size);
for (let i = 0; i < size; i++) {
let best = rand(pop.length);
for (let t = 0; t < 2; t++) { const c = rand(pop.length); if (fit[c] > fit[best]) best = c; }
next[i] = Math.random() < MUTATE ? rand(nStrat) : pop[best];
}
return next;
};
const initSim = (rounds: number, predIdx: number, bonus: number): SimState => {
const nStrat = STRATEGIES.length;
const pop = new Uint8Array(POP_SIZE).map(() => rand(nStrat));
return { gen: 0, pop, ...evaluate(pop, rounds, predIdx, bonus) };
};
const stepSim = (s: SimState, rounds: number, predIdx: number, bonus: number): SimState => {
const pop = evolve(s.pop, s.fit, POP_SIZE);
return { gen: s.gen + 1, pop, ...evaluate(pop, rounds, predIdx, bonus) };
};
const getDist = (pop: Uint8Array) => {
const c = Array(STRATEGIES.length).fill(0);
pop.forEach(i => c[i]++);
return Object.fromEntries(STRAT_NAMES.map((n, i) => [n, c[i]]));
};
function App() {
const [roundsIdx, setRoundsIdx] = useState(0);
const [predIdx, setPredIdx] = useState(0);
const [bonusIdx, setBonusIdx] = useState(0);
const [ui, setUI] = useState<UIState>(initUI);
const [running, setRunning] = useState(true);
const simRef = useRef<SimState | null>(null);
const roundsRef = useRef(roundsIdx); roundsRef.current = roundsIdx;
const predRef = useRef(predIdx); predRef.current = predIdx;
const bonusRef = useRef(bonusIdx); bonusRef.current = bonusIdx;
const runningRef = useRef(running); runningRef.current = running;
const isGod = !!PREDICTORS[predIdx].god;
const reset = (ri: number, pi: number, bi: number) => {
simRef.current = initSim(PREDICTORS[pi].god ? 1 : ROUNDS_OPTIONS[ri], pi, BONUS_OPTIONS[bi]);
setUI(initUI());
if (!running) setRunning(true);
};
useEffect(() => {
simRef.current = initSim(ROUNDS_OPTIONS[0], 0, BONUS_OPTIONS[0]);
setUI(u => ({ ...u, dist: getDist(simRef.current!.pop), avgAcc: avg(Array.from(simRef.current!.acc)) }));
}, []);
useEffect(() => {
if (!running) return;
let frameId: number, idleUntil = 0;
const tick = (time: number) => {
if (!runningRef.current || !simRef.current || time < idleUntil) { frameId = requestAnimationFrame(tick); return; }
const t0 = performance.now();
const pi = predRef.current;
const bonus = BONUS_OPTIONS[bonusRef.current];
const rounds = PREDICTORS[pi].god ? 1 : ROUNDS_OPTIONS[roundsRef.current];
for (let i = 0; i < 3; i++) simRef.current = stepSim(simRef.current!, rounds, pi, bonus);
const dist = getDist(simRef.current.pop), { gen, acc, fit } = simRef.current;
const avgAcc = avg(Array.from(acc)), avgPay = avg(Array.from(fit)) / rounds;
setUI(u => ({
gen, avgAcc, avgPay, dist,
history: [...u.history.slice(-99), { gen, avgAcc, avgPay, dist }],
totals: Object.fromEntries(STRAT_NAMES.map(n => [n, u.totals[n] + dist[n] * 3]))
}));
idleUntil = time + (performance.now() - t0) * (1 - CPU_THROTTLE) / CPU_THROTTLE;
frameId = requestAnimationFrame(tick);
};
frameId = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frameId);
}, [running]);
return (
<div className="container">
<h1><span className="gradient-text">Ideal vs Mortal Newcomb's</span></h1>
<p className="subtitle">Strategies evolve against a predictor. Two-boxing only wins when the predictor has no accuracy at all (try Mortal, 1 round). In classic Newcomb's, the predictor only needs 50.05% accuracy. Adjust Box A to narrow the payoff gap: the required accuracy rises, but two-boxing still can't dominate.</p>
<div className="config-row">
<div className="config-actions">
<button className="btn-circle" onClick={() => setRunning(!running)} title={running ? "Pause" : "Run"}>{running
? <svg width="14" height="14" viewBox="0 0 14 14"><rect x="1" y="1" width="4" height="12" rx="1" fill="currentColor"/><rect x="9" y="1" width="4" height="12" rx="1" fill="currentColor"/></svg>
: <svg width="18" height="18" viewBox="0 0 14 14" style={{marginLeft: 2}}><path d="M2 1l11 6-11 6z" fill="currentColor"/></svg>}</button>
<button className="btn-circle" onClick={() => reset(roundsIdx, predIdx, bonusIdx)} title="Reset">↻</button>
</div>
<div className="config-item">
<label className="config-label">Predictor</label>
<div className="radio-group">
{PREDICTORS.map((p, i) => (
<button key={p.name} className={`radio-btn${predIdx === i ? " active" : ""}`} onClick={() => { setPredIdx(i); if (PREDICTORS[i].god) { setRoundsIdx(0); setBonusIdx(0); } reset(PREDICTORS[i].god ? 0 : roundsIdx, i, PREDICTORS[i].god ? 0 : bonusIdx); }}>{p.name}</button>
))}
</div>
<span className="config-desc">{PREDICTORS[predIdx].desc}</span>
</div>
<div className="config-item">
<label className="config-label">Rounds</label>
<div className="radio-group">
{ROUNDS_OPTIONS.map((v, i) => (
<button key={v} className={`radio-btn${roundsIdx === i ? " active" : ""}${isGod && i > 0 ? " disabled" : ""}`}
disabled={isGod && i > 0}
onClick={() => { setRoundsIdx(i); if (i === 0) setBonusIdx(0); reset(i, predIdx, i === 0 ? 0 : bonusIdx); }}>{v}</button>
))}
</div>
</div>
<div className="config-item">
<label className="config-label">Box A</label>
<div className="radio-group">
{BONUS_OPTIONS.map((v, i) => (
<button key={v} className={`radio-btn${bonusIdx === i ? " active" : ""}${(isGod || roundsIdx === 0) && i > 0 ? " disabled" : ""}`}
disabled={(isGod || roundsIdx === 0) && i > 0}
onClick={() => { setBonusIdx(i); reset(roundsIdx, predIdx, i); }}>{fmtPay(v)}</button>
))}
</div>
</div>
</div>
<div className="controls">
<table className="payoff-table payoff-table--compact">
<thead><tr><th></th><th>Pred: 1</th><th>Pred: 2</th></tr></thead>
<tbody>
<tr><td><strong>1-Box</strong></td><td className="good">$1M</td><td className="bad">$0</td></tr>
<tr><td><strong>2-Box</strong></td><td className="tempt">{fmtPay(1000000 + BONUS_OPTIONS[bonusIdx])}</td><td className="neutral">{fmtPay(BONUS_OPTIONS[bonusIdx])}</td></tr>
</tbody>
</table>
<MiniPerfChart gen={ui.gen} history={ui.history} avgAcc={ui.avgAcc} avgPay={ui.avgPay} />
</div>
<section className="type-distribution">
<h2>Strategy Prevalence</h2>
<Chart history={ui.history} totals={ui.totals} popSize={POP_SIZE} />
</section>
</div>
);
}
function MiniPerfChart({ gen, history, avgAcc, avgPay }: { gen: number; history: HistEntry[]; avgAcc: number; avgPay: number }) {
const W = 180, H = 44, P = 2;
const sm = history.slice(-50).map((e, i, arr) => {
const win = arr.slice(Math.max(0, i - 2), i + 1);
return { acc: avg(win.map(h => h.avgAcc)), pay: avg(win.map(h => h.avgPay)) };
});
const maxPay = Math.max(...sm.map(s => s.pay), 1);
const xS = (i: number) => P + (i / Math.max(sm.length - 1, 1)) * (W - P * 2);
const yAcc = (v: number) => P + (H - P * 2) - (v / 100) * (H - P * 2);
const yPay = (v: number) => P + (H - P * 2) - (v / maxPay) * (H - P * 2);
const mkPath = (fn: (s: typeof sm[0]) => number) => sm.map((s, i) => `${i ? "L" : "M"}${xS(i)} ${fn(s)}`).join(" ");
return (
<div className="mini-perf">
<div className="mini-perf-labels">
<span className="mini-perf-val" style={{ color: "var(--text-secondary)" }}>Gen: {gen}</span>
<span className="mini-perf-val" style={{ color: "var(--accent-blue)" }}>Acc: {avgAcc.toFixed(0)}%</span>
<span className="mini-perf-val" style={{ color: "var(--accent-gold)" }}>Avg: {fmtPay(Math.round(avgPay))}</span>
</div>
<svg viewBox={`0 0 ${W} ${H}`} className="mini-perf-svg">
{sm.length > 1 && <>
<path d={mkPath(s => yAcc(s.acc))} fill="none" stroke="var(--accent-blue)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
<path d={mkPath(s => yPay(s.pay))} fill="none" stroke="var(--accent-gold)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</>}
</svg>
</div>
);
}
const SMOOTH_WINDOW = 5;
function Chart({ history, totals, popSize }: { history: HistEntry[]; totals: Record<string, number>; popSize: number }) {
const containerRef = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(600);
useEffect(() => {
if (!containerRef.current) return;
const obs = new ResizeObserver(([e]) => setWidth(Math.max(400, e.contentRect.width)));
obs.observe(containerRef.current);
return () => obs.disconnect();
}, []);
const [W, H] = [width, 250], P = { t: 20, r: Math.min(140, Math.max(100, W * 0.15)), b: 40, l: 50 };
const [cW, cH] = [W - P.l - P.r, H - P.t - P.b];
const total = sum(totals);
const ranked = STRAT_NAMES.map(n => ({ name: n, pct: total ? (totals[n] / total) * 100 : 0 })).sort((a, b) => b.pct - a.pct);
const smoothed = history.map((e, i) => ({
...e, dist: Object.fromEntries(STRAT_NAMES.map(n => [n, avg(history.slice(Math.max(0, i - SMOOTH_WINDOW + 1), i + 1).map(h => h.dist[n] || 0))]))
}));
const recent = history.slice(-SMOOTH_WINDOW);
const recentDist = Object.fromEntries(STRAT_NAMES.map(n => [n, recent.length ? Math.round(avg(recent.map(h => h.dist[n] || 0))) : 0]));
const [minG, maxG] = smoothed.length ? [smoothed[0].gen, smoothed.at(-1)!.gen] : [0, 1];
const range = Math.max(maxG - minG, 1);
const xS = (g: number) => P.l + ((g - minG) / range) * cW, yS = (c: number) => P.t + cH - (c / popSize) * cH;
const paths = STRATEGIES.map(({ name, color }) => {
const pts = smoothed.map(e => ({ x: xS(e.gen), y: yS(e.dist[name] || 0) }));
return pts.length ? { name, path: pts.map((pt, i) => `${i ? "L" : "M"}${pt.x} ${pt.y}`).join(" "), color, last: pts.at(-1)! } : null;
}).filter(Boolean) as { name: string; path: string; color: string; last: { x: number; y: number } }[];
const yTicks = [0, .25, .5, .75, 1].map(f => Math.round(f * popSize));
const xTicks = [...new Set(Array.from({ length: Math.min(5, range) + 1 }, (_, i) => Math.round(minG + (i / Math.min(5, range)) * range)))];
return (
<div className="line-chart-container" ref={containerRef}>
<svg viewBox={`0 0 ${W} ${H}`} className="line-chart-svg">
{!smoothed.length ? (
<text x={W / 2} y={H / 2} textAnchor="middle" fill="var(--text-secondary)" fontSize="14">Run simulation to see strategy evolution</text>
) : (<>
{yTicks.map(t => <line key={t} x1={P.l} y1={yS(t)} x2={P.l + cW} y2={yS(t)} stroke="var(--border-color)" strokeDasharray="3,3" />)}
<line x1={P.l} y1={P.t} x2={P.l} y2={P.t + cH} stroke="var(--text-secondary)" />
{yTicks.map(t => <text key={t} x={P.l - 10} y={yS(t) + 4} textAnchor="end" fill="var(--text-secondary)" fontSize="11">{t}</text>)}
<text x={15} y={P.t + cH / 2} textAnchor="middle" fill="var(--text-secondary)" fontSize="12" transform={`rotate(-90, 15, ${P.t + cH / 2})`}>Population</text>
<line x1={P.l} y1={P.t + cH} x2={P.l + cW} y2={P.t + cH} stroke="var(--text-secondary)" />
{xTicks.map(t => <text key={t} x={xS(t)} y={P.t + cH + 20} textAnchor="middle" fill="var(--text-secondary)" fontSize="11">{t}</text>)}
<text x={P.l + cW / 2} y={H - 5} textAnchor="middle" fill="var(--text-secondary)" fontSize="12">Generation</text>
{paths.map(p => (
<g key={p.name}>
<path d={p.path} fill="none" stroke={p.color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<circle cx={p.last.x} cy={p.last.y} r="4" fill={p.color} />
</g>
))}
{paths.filter(p => recentDist[p.name] > 0).sort((a, b) => recentDist[b.name] - recentDist[a.name]).map((p, i) => (
<g key={p.name} transform={`translate(${P.l + cW + 10}, ${P.t + i * 18})`}>
<line x1="0" y1="0" x2="15" y2="0" stroke={p.color} strokeWidth="2" />
<text x="20" y="4" fill="var(--text-primary)" fontSize="10">{p.name} ({recentDist[p.name]})</text>
</g>
))}
</>)}
</svg>
<div className="lifetime-table-container">
<h2 className="lifetime-table-title">Lifetime Dominance</h2>
<table className="lifetime-table">
<thead><tr><th className="col-color"></th><th className="col-strategy">Strategy</th><th className="col-description">Description</th><th className="col-dominance">Dominance</th></tr></thead>
<tbody>
{ranked.map(({ name, pct }) => (
<tr key={name} className={pct === 0 ? "row-zero" : ""}>
<td className="col-color"><span className="color-swatch" style={{ background: BY_NAME[name].color }} /></td>
<td className="col-strategy">{name}</td>
<td className="col-description">{BY_NAME[name].desc}</td>
<td className="col-dominance">
<div className="dominance-bar-container">
<div className="dominance-bar-track">
<div className="dominance-bar" style={{ width: `${pct}%`, background: BY_NAME[name].color }} />
</div>
<span className="dominance-value">{pct.toFixed(1)}%</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
createRoot(document.getElementById("root")!).render(<App />);
```
**styles.css**
```css
:root {
--bg-gradient-start: #1a1a1a;
--bg-gradient-end: #1a1a1a;
--text-primary: #e4e4e7;
--text-secondary: #9ca3af;
--accent-gold: #fbbf24;
--accent-green: #4ade80;
--accent-blue: #60a5fa;
--accent-purple: #c084fc;
--accent-red: #f87171;
--surface-overlay: rgba(255, 255, 255, 0.05);
--surface-overlay-dark: rgba(0, 0, 0, 0.3);
--border-color: rgba(255, 255, 255, 0.1);
}
html[data-theme="light"] {
--bg-gradient-start: #f0f4f8;
--bg-gradient-end: #e2e8f0;
--text-primary: #1a202c;
--text-secondary: #4a5568;
--accent-gold: #d97706;
--accent-green: #059669;
--accent-blue: #2563eb;
--accent-purple: #7c3aed;
--accent-red: #b91c1c;
--surface-overlay: rgba(255, 255, 255, 0.8);
--surface-overlay-dark: rgba(0, 0, 0, 0.05);
--border-color: rgba(0, 0, 0, 0.1);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
.container { max-width: 1200px; margin: 0 auto; }
h1 { text-align: center; font-size: 2.2rem; margin-bottom: 12px; }
.subtitle { text-align: center; max-width: 720px; margin: 0 auto 20px; font-size: 0.95rem; line-height: 1.5; color: var(--text-secondary); }
h1 .gradient-text { background: linear-gradient(90deg, var(--accent-gold), var(--accent-purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
h2 { font-size: 1.3rem; margin-bottom: 15px; color: var(--accent-gold); text-align: center; }
.config-label, .lifetime-table th {
font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;
}
.lifetime-table th { padding: 6px 10px; text-align: left; border-bottom: 2px solid var(--border-color); }
.config-row { display: flex; flex-wrap: wrap; gap: 12px 20px; background: var(--surface-overlay); padding: 10px 16px; border-radius: 10px; margin-bottom: 10px; align-items: flex-start; justify-content: center; }
.config-item { display: flex; flex-direction: column; gap: 4px; align-items: center; }
.config-desc { font-size: 0.8rem; color: var(--text-secondary); font-style: italic; }
.config-actions { display: flex; gap: 8px; align-items: center; padding-top: 1.1em; }
.radio-group { display: flex; gap: 4px; }
.radio-btn { padding: 5px 12px; border: 1px solid var(--border-color); border-radius: 6px; background: transparent; color: var(--text-secondary); font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: all 0.15s; }
.radio-btn:hover { border-color: var(--accent-blue); color: var(--text-primary); }
.radio-btn.active { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; }
.radio-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.controls { display: flex; flex-wrap: wrap; gap: 32px; align-items: center; justify-content: center; background: var(--surface-overlay); padding: 15px 20px; border-radius: 12px; margin-bottom: 20px; }
.dominance-bar-container { display: flex; align-items: center; }
.dominance-value { font-size: 0.85rem; min-width: 50px; }
.btn-circle { width: 32px; height: 32px; border: none; border-radius: 50%; background: var(--accent-blue); cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: transform 0.15s; line-height: 1; color: #fff; }
.btn-circle:hover { transform: scale(1.15); }
.mini-perf { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.mini-perf-svg { width: 180px; height: 58px; border-radius: 6px; background: var(--surface-overlay-dark); flex-shrink: 0; }
.mini-perf-labels { display: flex; flex-direction: column; gap: 2px; min-width: 100px; }
.mini-perf-val { font-size: 0.9rem; font-weight: 600; font-family: "SF Mono", "Monaco", "Consolas", monospace; font-variant-numeric: tabular-nums; white-space: nowrap; }
section { background: rgba(255, 255, 255, 0.03); border-radius: 16px; padding: 20px; margin-bottom: 20px; }
.payoff-table { border-collapse: collapse; }
.payoff-table th, .payoff-table td { padding: 12px 20px; border: 1px solid var(--border-color); }
.payoff-table th { background: var(--surface-overlay-dark); }
.payoff-table--compact th, .payoff-table--compact td { padding: 2px 6px; white-space: nowrap; text-align: center; }
.payoff-table--compact { font-size: 0.9rem; width: fit-content; table-layout: auto; }
.payoff-table .good { background: rgba(74, 222, 128, 0.2); color: var(--accent-green); }
.payoff-table .bad { background: rgba(248, 113, 113, 0.2); color: var(--accent-red); }
.payoff-table .tempt { background: rgba(251, 191, 36, 0.2); color: var(--accent-gold); }
.payoff-table .neutral { background: rgba(156, 163, 175, 0.2); color: var(--text-secondary); }
.type-distribution { padding: 12px 16px; }
.line-chart-container { display: flex; flex-direction: column; gap: 12px; width: 100%; }
.line-chart-svg { width: 100%; height: auto; min-height: 250px; aspect-ratio: auto; border-radius: 12px; background: var(--surface-overlay-dark); }
.lifetime-table-container { margin-top: 12px; overflow: hidden; }
.lifetime-table-title { margin-top: 8px; }
.lifetime-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
.lifetime-table td { padding: 8px 10px; border-bottom: 1px solid var(--border-color); vertical-align: middle; }
.lifetime-table tr:last-child td { border-bottom: none; }
.lifetime-table tr:hover:not(.row-zero) { background: var(--surface-overlay); }
.lifetime-table .row-zero { opacity: 0.4; }
.lifetime-table .col-color { width: 30px; text-align: center; }
.lifetime-table .col-strategy { width: 160px; font-weight: 500; color: var(--text-primary); white-space: nowrap; }
.lifetime-table .col-description { color: var(--text-secondary); font-size: 0.8rem; }
.lifetime-table .col-dominance { width: 180px; min-width: 180px; }
.color-swatch { display: inline-block; width: 14px; height: 14px; border-radius: 4px; }
.dominance-bar-container { gap: 8px; width: 100%; }
.dominance-bar-track { flex: 1; min-width: 60px; height: 8px; background: var(--surface-overlay-dark); border-radius: 4px; overflow: hidden; position: relative; }
.dominance-bar { height: 100%; border-radius: 4px; min-width: 1px; position: absolute; left: 0; top: 0; }
@media (max-width: 768px) {
h1 { font-size: 1.6rem; }
.config-row { gap: 10px 12px; }
.controls { gap: 10px; }
.lifetime-table .col-description { display: none; }
.lifetime-table .col-strategy { width: auto; }
.lifetime-table .col-dominance { width: 140px; min-width: 140px; }
.dominance-bar-track { min-width: 40px; }
.lifetime-table th { padding: 5px 8px; font-size: 0.7rem; }
.lifetime-table td { padding: 6px 8px; font-size: 0.8rem; }
.dominance-bar-track { display: none; }
}
```
**index.html**
```html
<body>
<div id="root"></div>
</body>
```
**config.json**
```json
{
"dependencies": {
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"description": "Explore how predictor accuracy and payoff ratios interact in Newcomb's problem. Two-boxing only wins when prediction is no better than chance."
}
```