---
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."
}
```