{
  "schema": "app-studio/canvas@1",
  "name": "Cooperation Simulator.html",
  "code": "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>Cooperation Simulator — a KIT App by Halison</title>\n<!--\n  Cooperation Simulator · a KIT App by Halison\n\n  A self-contained iterated prisoner's dilemma tournament. No build step, no\n  network calls, no dependencies: one file that runs the same from a web\n  preview, from disk, and from inside KIT Apps.\n\n  ?embed=1 skips the splash. The splash is the right first screen for a file\n  somebody has downloaded and opened on its own, and pure friction when the\n  thing is already sitting inside an article that just explained it.\n-->\n<style>\n  :root {\n    --bg: #07070a;\n    --panel: #101018;\n    --panel-2: #16161f;\n    --line: rgba(255,255,255,0.09);\n    --fg: #f4f4f6;\n    --dim: #9a9aa8;\n    --indigo: #6E97FF;\n    --coop: #4ade80;\n    --defect: #f87171;\n    --font: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n    --mono: var(--font);\n  }\n  * { box-sizing: border-box; }\n  html { color-scheme: dark; }\n  * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.2) transparent; }\n  ::-webkit-scrollbar { width: 8px; height: 8px; }\n  ::-webkit-scrollbar-track { background: transparent; }\n  ::-webkit-scrollbar-thumb {\n    background: rgba(255,255,255,0.16); border-radius: 99px;\n    border: 2px solid transparent; background-clip: content-box;\n  }\n  ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.28); background-clip: content-box; }\n  ::-webkit-scrollbar-corner { background: transparent; }\n  html.embed, html.embed body { font-variant-numeric: tabular-nums; height: auto; min-height: 0; overflow: visible; }\n  html.embed #splash { display: none !important; }\n  html.embed header { display: none; }\n  html.embed .app, html.embed .app.on { min-height: 0; height: auto; }\n  html.embed main { min-height: 0; }\n  html.embed .results, html.embed .controls { overflow: visible; }\n  @media (max-width: 760px) {\n    html.embed .controls { padding: 14px 16px 12px; }\n    html.embed .group { margin-bottom: 14px; }\n    html.embed .strats { grid-template-columns: 1fr 1fr; }\n    html.embed .results { padding: 16px; }\n  }\n  html, body { height: 100%; }\n  body {\n    margin: 0; background: var(--bg); color: var(--fg);\n    font-family: var(--font); font-size: 14px; line-height: 1.5;\n    -webkit-font-smoothing: antialiased;\n  }\n  button { font: inherit; color: inherit; cursor: pointer; }\n\n  /* ── Splash ─────────────────────────────────────────────────────── */\n  #splash {\n    position: fixed; inset: 0; z-index: 50; display: grid; place-items: center;\n    background: radial-gradient(900px 500px at 50% 22%, rgba(110,151,255,0.16), transparent 70%), var(--bg);\n    text-align: center; padding: 24px;\n  }\n  #splash .inner { max-width: 520px; }\n  #splash h1 {\n    font-size: clamp(26px, 5vw, 38px); font-weight: 800; letter-spacing: -0.03em;\n    text-transform: uppercase; margin: 0 0 14px;\n  }\n  #splash p { color: var(--dim); font-size: 15px; margin: 0 0 30px; }\n  #splash .by { font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--dim); margin-bottom: 26px; }\n  .btn-primary {\n    background: var(--indigo); color: #07070a; border: 0; font-weight: 700;\n    padding: 11px 30px; border-radius: 999px; letter-spacing: 0.01em;\n  }\n  .btn-primary:hover { filter: brightness(1.1); }\n\n  /* ── Chrome ─────────────────────────────────────────────────────── */\n  .app { display: none; min-height: 100%; flex-direction: column; }\n  .app.on { display: flex; }\n  header {\n    display: flex; align-items: center; gap: 16px; flex-wrap: wrap;\n    padding: 14px 20px; border-bottom: 1px solid var(--line);\n  }\n  header h2 { margin: 0; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }\n  header .sub { color: var(--dim); font-size: 12px; }\n  .spacer { flex: 1 1 auto; }\n  .attrib {\n    font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase;\n    color: var(--dim); background: none; border: 1px solid var(--line);\n    padding: 5px 11px; border-radius: 999px;\n  }\n  .attrib:hover { color: var(--fg); border-color: rgba(255,255,255,0.25); }\n\n  main { display: grid; grid-template-columns: 260px 1fr; flex: 1 1 auto; min-height: 0; }\n  @media (max-width: 760px) { main { grid-template-columns: 1fr; } }\n\n  /* ── Controls ───────────────────────────────────────────────────── */\n  .controls { border-right: 1px solid var(--line); padding: 18px 18px 26px; }\n  @media (max-width: 760px) { .controls { border-right: 0; border-bottom: 1px solid var(--line); } }\n  .group { margin-bottom: 22px; }\n  .group > label, .legend {\n    display: block; font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;\n    color: var(--dim); margin-bottom: 9px;\n  }\n  .row { display: flex; align-items: center; gap: 10px; }\n  input[type=range] { width: 100%; accent-color: var(--indigo); }\n  .val { font-family: var(--mono); font-size: 12px; min-width: 46px; text-align: right; color: var(--fg); }\n  .modes { display: flex; gap: 6px; }\n  .modes button {\n    flex: 1; background: var(--panel); border: 1px solid var(--line); color: var(--dim);\n    padding: 7px 0; border-radius: 9px; font-size: 12px; font-weight: 600;\n  }\n  .modes button[aria-pressed=\"true\"] { background: var(--indigo); border-color: var(--indigo); color: #07070a; }\n  .strats { display: grid; gap: 4px; }\n  .strats label {\n    display: flex; align-items: center; gap: 9px; font-size: 12.5px;\n    padding: 5px 7px; border-radius: 7px; cursor: pointer; color: var(--dim);\n  }\n  .strats label:hover { background: var(--panel); color: var(--fg); }\n  .strats input { accent-color: var(--indigo); }\n  .strats .sw { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 auto; }\n  .run { width: 100%; margin-top: 4px; }\n\n  /* ── Results ────────────────────────────────────────────────────── */\n  .results { padding: 20px; overflow: auto; }\n  .headline { font-size: 13px; color: var(--dim); margin: 0 0 16px; }\n  .headline b { color: var(--fg); }\n  table { width: 100%; border-collapse: collapse; }\n  th { text-align: left; font-size: 10px; letter-spacing: 0.18em; text-transform: uppercase; color: var(--dim); font-weight: 600; padding-bottom: 9px; }\n  th.num, td.num { text-align: right; font-family: var(--mono); }\n  td { padding: 9px 0; border-top: 1px solid var(--line); font-size: 13px; vertical-align: middle; }\n  .bar { height: 7px; border-radius: 4px; background: var(--indigo); min-width: 2px; }\n  .barcell { width: 42%; padding-right: 14px; }\n  .name { display: flex; align-items: center; gap: 9px; }\n  .rank { font-family: var(--mono); color: var(--dim); font-size: 11px; width: 18px; }\n\n  .chart { width: 100%; height: 260px; display: block; margin-top: 6px; }\n  .legend-row { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 12px; }\n  .legend-row span { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--dim); }\n  .note { margin-top: 20px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--dim); font-size: 12.5px; max-width: 62ch; }\n\n  /* ── About ──────────────────────────────────────────────────────── */\n  #about { position: fixed; inset: 0; z-index: 60; display: none; place-items: center; background: rgba(0,0,0,0.72); padding: 20px; }\n  #about.on { display: grid; }\n  .card { background: var(--panel-2); border: 1px solid var(--line); border-radius: 18px; padding: 26px; max-width: 460px; width: 100%; }\n  .card h3 { margin: 0 0 4px; font-size: 17px; letter-spacing: -0.01em; }\n  .card .kicker { font-size: 10px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--dim); margin-bottom: 14px; }\n  .card p { color: var(--dim); font-size: 13px; margin: 0 0 14px; }\n  .card dl { display: grid; grid-template-columns: auto 1fr; gap: 6px 18px; font-size: 12.5px; margin: 0 0 18px; }\n  .card dt { color: var(--dim); }\n  .card dd { margin: 0; font-family: var(--mono); }\n  .card a { color: var(--indigo); text-decoration: none; }\n  .card a:hover { text-decoration: underline; }\n  .card .actions { display: flex; gap: 10px; flex-wrap: wrap; }\n  .btn-ghost { background: none; border: 1px solid var(--line); padding: 8px 16px; border-radius: 999px; font-size: 13px; }\n  .btn-ghost:hover { border-color: rgba(255,255,255,0.28); }\n</style>\n</head>\n<body>\n\n<div id=\"splash\">\n  <div class=\"inner\">\n    <div class=\"by\">A KIT App by Halison</div>\n    <h1>Cooperation Simulator</h1>\n    <p>Explore how strategies evolve in repeated games. Everything runs on this machine — no account, no network.</p>\n    <button class=\"btn-primary\" id=\"start\">Start</button>\n  </div>\n</div>\n\n<div class=\"app\" id=\"app\">\n  <header>\n    <div>\n      <h2>Cooperation Simulator</h2>\n      <div class=\"sub\">Iterated prisoner's dilemma · round-robin tournament</div>\n    </div>\n    <div class=\"spacer\"></div>\n    <button class=\"attrib\" id=\"openAbout\">KIT Apps · Halison</button>\n  </header>\n\n  <main>\n    <section class=\"controls\">\n      <div class=\"group\">\n        <label>Mode</label>\n        <div class=\"modes\">\n          <button id=\"modeTournament\" aria-pressed=\"true\">Tournament</button>\n          <button id=\"modeEvolution\" aria-pressed=\"false\">Evolution</button>\n        </div>\n      </div>\n\n      <div class=\"group\">\n        <label for=\"rounds\">Rounds per pairing</label>\n        <div class=\"row\"><input type=\"range\" id=\"rounds\" min=\"1\" max=\"200\" value=\"60\" /><span class=\"val\" id=\"roundsVal\">60</span></div>\n      </div>\n\n      <div class=\"group\">\n        <label for=\"noise\">Chance a move is misread</label>\n        <div class=\"row\"><input type=\"range\" id=\"noise\" min=\"0\" max=\"25\" value=\"0\" /><span class=\"val\" id=\"noiseVal\">0%</span></div>\n      </div>\n\n      <div class=\"group\" id=\"gensGroup\" hidden>\n        <label for=\"gens\">Generations</label>\n        <div class=\"row\"><input type=\"range\" id=\"gens\" min=\"2\" max=\"60\" value=\"25\" /><span class=\"val\" id=\"gensVal\">25</span></div>\n      </div>\n\n      <div class=\"group\">\n        <label>Strategies in play</label>\n        <div class=\"strats\" id=\"strats\"></div>\n      </div>\n\n      <button class=\"btn-primary run\" id=\"run\">Run</button>\n    </section>\n\n    <section class=\"results\" id=\"results\"></section>\n  </main>\n</div>\n\n<div id=\"about\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"aboutTitle\">\n  <div class=\"card\">\n    <div class=\"kicker\">About this KIT App</div>\n    <h3 id=\"aboutTitle\">Cooperation Simulator</h3>\n    <p>A round-robin tournament for the repeated prisoner's dilemma. Strategies play every other strategy, and the scores show which behaviours survive once the game is played more than once.</p>\n    <dl>\n      <dt>Version</dt><dd>1.0.0</dd>\n      <dt>Publisher</dt><dd>Halison</dd>\n      <dt>Format</dt><dd>.kitapp</dd>\n      <dt>Runs</dt><dd>locally</dd>\n    </dl>\n    <p>Built to accompany the article <a href=\"https://halison.io/articles/why-cooperation-survives-when-cheating-pays\" target=\"_blank\" rel=\"noopener\">Why cooperation survives when cheating pays</a>.</p>\n    <div class=\"actions\">\n      <a class=\"btn-ghost\" href=\"https://halison.io/resources/tools\" target=\"_blank\" rel=\"noopener\">More KIT Apps</a>\n      <button class=\"btn-ghost\" id=\"closeAbout\">Close</button>\n    </div>\n  </div>\n</div>\n\n<script>\n(() => {\n  \"use strict\";\n\n  /* ── Payoffs ──────────────────────────────────────────────────────\n     The ordering is the entire dilemma: T > R > P > S, and 2R > T + S so\n     that mutual cooperation beats taking turns exploiting each other. */\n  const T = 5, R = 3, P = 1, S = 0;\n  const C = \"C\", D = \"D\";\n\n  /* ── Strategies ───────────────────────────────────────────────────\n     Each is a pure function of the history it can actually see: its own\n     past moves and what it *observed* of the opponent's, which under noise\n     is not the same as what the opponent played. */\n  const STRATEGIES = [\n    { id: \"tft\",      name: \"Tit for Tat\",         color: \"#6E97FF\", on: true,\n      desc: \"Cooperates first, then copies whatever it last saw.\",\n      move: (me, them) => (them.length === 0 ? C : them[them.length - 1]) },\n\n    { id: \"tf2t\",     name: \"Tit for Two Tats\",    color: \"#a78bfa\", on: true,\n      desc: \"Only retaliates after two defections in a row.\",\n      move: (me, them) => (them.length >= 2 && them[them.length - 1] === D && them[them.length - 2] === D ? D : C) },\n\n    { id: \"gtft\",     name: \"Generous Tit for Tat\", color: \"#22d3ee\", on: true,\n      desc: \"Tit for Tat, but forgives one defection in ten.\",\n      move: (me, them, rnd) => (them.length === 0 ? C : them[them.length - 1] === D && rnd() < 0.1 ? C : them[them.length - 1]) },\n\n    { id: \"pavlov\",   name: \"Pavlov\",              color: \"#4ade80\", on: true,\n      desc: \"Repeats its last move if it did well, switches if it did badly.\",\n      move: (me, them) => {\n        if (me.length === 0) return C;\n        const my = me[me.length - 1], their = them[them.length - 1];\n        const goodOutcome = (my === C && their === C) || (my === D && their === C);\n        return goodOutcome ? my : (my === C ? D : C);\n      } },\n\n    { id: \"grudger\",  name: \"Grudger\",             color: \"#fbbf24\", on: true,\n      desc: \"Cooperates until crossed once, then never again.\",\n      move: (me, them) => (them.includes(D) ? D : C) },\n\n    { id: \"alwaysc\",  name: \"Always Cooperate\",    color: \"#34d399\", on: true,\n      desc: \"Never defects, whatever happens.\",\n      move: () => C },\n\n    { id: \"alwaysd\",  name: \"Always Defect\",       color: \"#f87171\", on: true,\n      desc: \"Never cooperates.\",\n      move: () => D },\n\n    { id: \"random\",   name: \"Random\",              color: \"#94a3b8\", on: true,\n      desc: \"Flips a coin every round.\",\n      move: (me, them, rnd) => (rnd() < 0.5 ? C : D) },\n  ];\n\n  /* Seeded PRNG (mulberry32) so a given configuration always gives the same\n     answer. A simulation you cannot reproduce is an anecdote. */\n  function rng(seed) {\n    let a = seed >>> 0;\n    return function () {\n      a = (a + 0x6D2B79F5) >>> 0;\n      let t = Math.imul(a ^ (a >>> 15), 1 | a);\n      t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n    };\n  }\n\n  /** One pairing. Returns [scoreA, scoreB]. */\n  function play(a, b, rounds, noise, rnd) {\n    const aMoves = [], bMoves = [];        // what each actually played\n    const aSeen = [], bSeen = [];          // what each opponent observed\n    let scoreA = 0, scoreB = 0;\n\n    for (let i = 0; i < rounds; i++) {\n      const am = a.move(aMoves, bSeen, rnd);\n      const bm = b.move(bMoves, aSeen, rnd);\n      aMoves.push(am); bMoves.push(bm);\n\n      // Noise corrupts observation, not intent: the payoff follows the real\n      // move, while the opponent reacts to the garbled one. That asymmetry is\n      // what makes unforgiving strategies collapse under noise.\n      const aObserved = noise > 0 && rnd() < noise ? (am === C ? D : C) : am;\n      const bObserved = noise > 0 && rnd() < noise ? (bm === C ? D : C) : bm;\n      aSeen.push(aObserved); bSeen.push(bObserved);\n\n      if (am === C && bm === C) { scoreA += R; scoreB += R; }\n      else if (am === C && bm === D) { scoreA += S; scoreB += T; }\n      else if (am === D && bm === C) { scoreA += T; scoreB += S; }\n      else { scoreA += P; scoreB += P; }\n    }\n    return [scoreA, scoreB];\n  }\n\n  /** Round robin, including each strategy against a copy of itself. */\n  function tournament(list, rounds, noise, rnd) {\n    const totals = new Map(list.map((s) => [s.id, 0]));\n    for (let i = 0; i < list.length; i++) {\n      for (let j = i; j < list.length; j++) {\n        const [sa, sb] = play(list[i], list[j], rounds, noise, rnd);\n        totals.set(list[i].id, totals.get(list[i].id) + sa);\n        totals.set(list[j].id, totals.get(list[j].id) + sb);\n      }\n    }\n    return list\n      .map((s) => ({ s, score: totals.get(s.id), perRound: totals.get(s.id) / (rounds * list.length) }))\n      .sort((x, y) => y.score - x.score);\n  }\n\n  /**\n   * Evolution: population shares update in proportion to average payoff\n   * (the replicator dynamic, discretised). A strategy that scores above the\n   * population average grows; one below it shrinks.\n   */\n  function evolve(list, rounds, noise, generations, rnd) {\n    let share = new Map(list.map((s) => [s.id, 1 / list.length]));\n    const history = [new Map(share)];\n\n    for (let g = 0; g < generations; g++) {\n      const payoff = new Map(list.map((s) => [s.id, 0]));\n      for (let i = 0; i < list.length; i++) {\n        for (let j = 0; j < list.length; j++) {\n          const [sa] = play(list[i], list[j], rounds, noise, rnd);\n          payoff.set(list[i].id, payoff.get(list[i].id) + (sa / rounds) * share.get(list[j].id));\n        }\n      }\n      const mean = list.reduce((acc, s) => acc + payoff.get(s.id) * share.get(s.id), 0);\n      const next = new Map();\n      let sum = 0;\n      for (const s of list) {\n        // mean > 0 always holds here: the worst possible payoff is P = 1.\n        const v = Math.max(0, share.get(s.id) * (payoff.get(s.id) / mean));\n        next.set(s.id, v); sum += v;\n      }\n      for (const s of list) next.set(s.id, next.get(s.id) / sum);\n      share = next;\n      history.push(new Map(share));\n    }\n    return history;\n  }\n\n  /* ── Wiring ───────────────────────────────────────────────────────── */\n  const $ = (id) => document.getElementById(id);\n  let mode = \"tournament\";\n\n  if (new URLSearchParams(location.search).has(\"embed\")) {\n    document.documentElement.classList.add(\"embed\");\n    $(\"splash\").style.display = \"none\";\n    $(\"app\").classList.add(\"on\");\n    const sendHeight = () => {\n      const h = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);\n      parent.postMessage({ type: \"kitapp:height\", height: Math.ceil(h) }, \"*\");\n    };\n    if (typeof ResizeObserver !== \"undefined\") new ResizeObserver(sendHeight).observe(document.body);\n    window.addEventListener(\"load\", sendHeight);\n    sendHeight();\n    setTimeout(sendHeight, 60);\n    setTimeout(sendHeight, 250);\n    setTimeout(sendHeight, 800);\n  }\n  $(\"start\").addEventListener(\"click\", () => {\n    $(\"splash\").style.display = \"none\";\n    $(\"app\").classList.add(\"on\");\n    run();\n  });\n\n  $(\"openAbout\").addEventListener(\"click\", () => $(\"about\").classList.add(\"on\"));\n  $(\"closeAbout\").addEventListener(\"click\", () => $(\"about\").classList.remove(\"on\"));\n  $(\"about\").addEventListener(\"click\", (e) => { if (e.target.id === \"about\") $(\"about\").classList.remove(\"on\"); });\n  document.addEventListener(\"keydown\", (e) => { if (e.key === \"Escape\") $(\"about\").classList.remove(\"on\"); });\n\n  const stratsEl = $(\"strats\");\n  for (const s of STRATEGIES) {\n    const label = document.createElement(\"label\");\n    label.title = s.desc;\n    label.innerHTML =\n      '<input type=\"checkbox\" data-id=\"' + s.id + '\"' + (s.on ? \" checked\" : \"\") + ' />' +\n      '<span class=\"sw\" style=\"background:' + s.color + '\"></span>' +\n      '<span>' + s.name + '</span>';\n    stratsEl.appendChild(label);\n  }\n\n  function selected() {\n    const ids = [...stratsEl.querySelectorAll(\"input:checked\")].map((i) => i.dataset.id);\n    return STRATEGIES.filter((s) => ids.includes(s.id));\n  }\n\n  function bindRange(id, valId, fmt) {\n    const input = $(id), out = $(valId);\n    const sync = () => { out.textContent = fmt(input.value); };\n    input.addEventListener(\"input\", () => { sync(); run(); });\n    sync();\n  }\n  bindRange(\"rounds\", \"roundsVal\", (v) => v);\n  bindRange(\"noise\", \"noiseVal\", (v) => v + \"%\");\n  bindRange(\"gens\", \"gensVal\", (v) => v);\n\n  $(\"modeTournament\").addEventListener(\"click\", () => setMode(\"tournament\"));\n  $(\"modeEvolution\").addEventListener(\"click\", () => setMode(\"evolution\"));\n  function setMode(m) {\n    mode = m;\n    $(\"modeTournament\").setAttribute(\"aria-pressed\", String(m === \"tournament\"));\n    $(\"modeEvolution\").setAttribute(\"aria-pressed\", String(m === \"evolution\"));\n    $(\"gensGroup\").hidden = m !== \"evolution\";\n    run();\n  }\n\n  stratsEl.addEventListener(\"change\", run);\n  $(\"run\").addEventListener(\"click\", run);\n\n  /* ── Rendering ────────────────────────────────────────────────────── */\n  function esc(s) { return String(s).replace(/[&<>\"]/g, (c) => ({ \"&\": \"&amp;\", \"<\": \"&lt;\", \">\": \"&gt;\", '\"': \"&quot;\" }[c])); }\n\n  function run() {\n    const list = selected();\n    const results = $(\"results\");\n    if (list.length < 2) {\n      results.innerHTML = '<p class=\"headline\">Pick at least two strategies — the dilemma needs someone to play against.</p>';\n      return;\n    }\n    const rounds = +$(\"rounds\").value;\n    const noise = +$(\"noise\").value / 100;\n    // Seed from the configuration, so the same settings always reproduce.\n    const seed = rounds * 7919 + Math.round(noise * 1000) * 104729 + list.length * 31;\n\n    if (mode === \"tournament\") renderTournament(list, rounds, noise, seed);\n    else renderEvolution(list, rounds, noise, +$(\"gens\").value, seed);\n  }\n\n  function renderTournament(list, rounds, noise, seed) {\n    const table = tournament(list, rounds, noise, rng(seed));\n    const top = table[0], max = top.score;\n    const nice = (n) => n.toFixed(2);\n\n    let html =\n      '<p class=\"headline\"><b>' + esc(top.s.name) + '</b> wins with ' + nice(top.perRound) +\n      ' points per round, over ' + rounds + ' round' + (rounds === 1 ? \"\" : \"s\") + ' per pairing' +\n      (noise > 0 ? ' and ' + Math.round(noise * 100) + '% misread moves' : '') + '.</p>' +\n      '<table><thead><tr><th>Strategy</th><th class=\"barcell\"></th><th class=\"num\">Per round</th><th class=\"num\">Total</th></tr></thead><tbody>';\n\n    table.forEach((row, i) => {\n      const w = Math.max(2, (row.score / max) * 100);\n      html +=\n        '<tr><td><span class=\"name\"><span class=\"rank\">' + (i + 1) + '</span>' +\n        '<span class=\"sw\" style=\"background:' + row.s.color + '\"></span>' + esc(row.s.name) + '</span></td>' +\n        '<td class=\"barcell\"><div class=\"bar\" style=\"width:' + w + '%;background:' + row.s.color + '\"></div></td>' +\n        '<td class=\"num\">' + nice(row.perRound) + '</td>' +\n        '<td class=\"num\">' + row.score + '</td></tr>';\n    });\n    html += \"</tbody></table>\";\n\n    const place = (id) => { const i = table.findIndex((r) => r.s.id === id); return i < 0 ? null : i + 1; };\n    const nasty = place(\"alwaysd\");\n    const tft = place(\"tft\");\n    const grudge = place(\"grudger\");\n\n    let note;\n    if (rounds <= 3) {\n      note = \"Over very few rounds there is no future to protect, and defection is simply the better move. This is the one-shot dilemma with extra steps.\";\n    } else if (noise >= 0.05) {\n      note = \"Noise changes who wins. A single misread move puts two strategies that were both trying to cooperate into mutual punishment, and the ones that never forgive cannot get out of it. Watch Grudger fall and the forgiving strategies rise as you turn this up.\";\n    } else if (grudge && tft && grudge < tft) {\n      note = \"Grudger edges out Tit for Tat here, which is worth knowing: with no noise, never forgiving costs nothing, because nobody ever defects by accident. Add a few percent of misread moves and that advantage inverts.\";\n    } else {\n      note = \"The strategies at the top are the ones that are never first to defect, retaliate when they are crossed, and go back to cooperating afterwards.\";\n    }\n    if (nasty) note += \" Always Defect finishes \" + nasty + \" of \" + table.length + \".\";\n    html += '<p class=\"note\">' + note + '</p>';\n\n    $(\"results\").innerHTML = html;\n  }\n\n  function renderEvolution(list, rounds, noise, gens, seed) {\n    const history = evolve(list, rounds, noise, gens, rng(seed));\n    const last = history[history.length - 1];\n    const ranked = [...list].sort((a, b) => last.get(b.id) - last.get(a.id));\n    const winner = ranked[0];\n    const pct = (v) => (v * 100).toFixed(1) + \"%\";\n\n    const W = 640, H = 240, PAD = 28;\n    const x = (i) => PAD + (i / (history.length - 1)) * (W - PAD * 2);\n    const y = (v) => H - PAD - v * (H - PAD * 2);\n\n    let paths = \"\";\n    for (const s of list) {\n      let d = \"\";\n      history.forEach((h, i) => { d += (i ? \"L\" : \"M\") + x(i).toFixed(1) + \" \" + y(h.get(s.id)).toFixed(1) + \" \"; });\n      paths += '<path d=\"' + d + '\" fill=\"none\" stroke=\"' + s.color + '\" stroke-width=\"2\" stroke-linejoin=\"round\" />';\n    }\n\n    let html =\n      '<p class=\"headline\">After ' + gens + ' generations the population is <b>' + pct(last.get(winner.id)) +\n      ' ' + esc(winner.name) + '</b>.</p>' +\n      '<svg class=\"chart\" viewBox=\"0 0 ' + W + ' ' + H + '\" preserveAspectRatio=\"none\" role=\"img\" aria-label=\"Population share by generation\">' +\n      '<line x1=\"' + PAD + '\" y1=\"' + y(0) + '\" x2=\"' + (W - PAD) + '\" y2=\"' + y(0) + '\" stroke=\"rgba(255,255,255,0.12)\" />' +\n      '<line x1=\"' + PAD + '\" y1=\"' + y(1) + '\" x2=\"' + (W - PAD) + '\" y2=\"' + y(1) + '\" stroke=\"rgba(255,255,255,0.06)\" />' +\n      paths + '</svg>' +\n      '<div class=\"legend-row\">' +\n      ranked.map((s) => '<span><span class=\"sw\" style=\"background:' + s.color + '\"></span>' + esc(s.name) + ' ' + pct(last.get(s.id)) + '</span>').join(\"\") +\n      '</div>' +\n      '<p class=\"note\">Each generation, every strategy reproduces in proportion to how well it scored against the population as it currently stands. Exploitative strategies do well while there are cooperators to exploit, and starve once there are not.</p>';\n\n    $(\"results\").innerHTML = html;\n  }\n\n  run();\n})();\n</script>\n</body>\n</html>\n",
  "packaging": {
    "appName": "Cooperation Simulator",
    "windowTitle": "Cooperation Simulator",
    "bundleId": "com.halisonworks.cooperationsimulator",
    "iconPath": null
  },
  "category": {
    "name": "Game theory",
    "tint": "indigo"
  },
  "sharedAt": "2026-09-16T00:00:00.000Z",
  "watermark": "Made with KIT Apps by Halison.io"
}
