/* v1 — dark tech showcase, built around the before/after pairs (static/data/evo.json). */ Evo.boot(({ games, arms }) => { const { $, h, s100, i100, signed, deltaClass, MODEL_CLASS, CATS } = Evo; const gain = (g) => g.repaired.reward - g.reward; const mean = Evo.mean; const dot = (model) => h("i", { class: `dot ${MODEL_CLASS[model] || ""}` }); const nav = $("#nav"); const onScroll = () => nav.classList.toggle("scrolled", scrollY > 40); addEventListener("scroll", onScroll, { passive: true }); onScroll(); const hv = $("#hero-video"); Evo.mountHeroVideo(hv, { src: hv.dataset.src, poster: hv.dataset.poster }); // ---------- first screen: a real game, already running ---------- (async function stage() { const cfg = window.EVO_PLAY || await fetch(Evo.data("play.json")).then((r) => (r.ok ? r.json() : null)).catch(() => null); const game = cfg?.games?.[0]; const el = $("#stage"); if (!game || !el) return; // the old pck player was removed; the node may be absent const box = $("#stage-game"), rounds = $("#stage-rounds"); const ROUNDS = [["r0", "Round 0"], ["r30", "Round 30"]]; const label = (r) => ROUNDS.find(([k]) => k === r)[1]; const canPlay = matchMedia("(pointer: fine)").matches && !navigator.connection?.saveData; let sel = "r30", frame = null; $("#stage-title").textContent = canPlay ? `You're playing ${game.title}` : `${game.title}: round 0 → round 30`; $("#stage-who").replaceChildren(dot(game.model), `made by ${game.model}, then improved over 30 RSIGame rounds`); $("#stage-keys").textContent = canPlay ? game.controls : ""; function run() { const b = game.builds[sel]; const src = "play.html?" + new URLSearchParams({ engine: Evo.media(cfg.engine.base), wasm_gz: cfg.engine.wasm_gz, pck: Evo.media(b.pck), pck_size: b.size, title: `${game.title} · ${label(sel)}`, poster: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`), }); frame = h("iframe", { src, title: `${game.title}, ${label(sel)}`, allow: "autoplay; fullscreen; gamepad" }); const hint = h("div", { class: "stage-hint" }, `Live game · ${game.controls}`); frame.addEventListener("load", () => frame.contentWindow?.focus()); box.replaceChildren(frame, hint); const dismiss = () => hint.classList.add("gone"); setTimeout(dismiss, 6000); frame.addEventListener("load", () => frame.contentWindow?.addEventListener("keydown", dismiss, { once: true })); } function watch() { // touch screens and data saver: the same two builds, replayed const wipe = Evo.wipeFrame(game.pair, { alt: game.title, eager: true, cls: "wipe stage-wipe", after: "Round 30" }); box.replaceChildren(wipe); Evo.observe(wipe); } rounds.replaceChildren(...ROUNDS.map(([r, t]) => h("button", { type: "button", class: r === sel ? "on" : "", "data-r": r, onclick: () => { if (sel === r) return; sel = r; rounds.querySelectorAll("button").forEach((x) => x.classList.toggle("on", x.dataset.r === r)); if (canPlay) run(); }, }, t, h("b", {}, s100(game.scores[r]))))); if (!canPlay) rounds.hidden = true; el.hidden = false; canPlay ? run() : watch(); // hide the site nav while the game has the screen; free the game once it is far out of view new IntersectionObserver(([e]) => { document.body.classList.toggle("on-stage", e.intersectionRatio > 0.6); if (e.intersectionRatio > 0.2) $("#hero-video").pause(); }, { threshold: [0, 0.2, 0.6, 1] }).observe(el); new IntersectionObserver(([e]) => { if (!canPlay) return; if (!e.isIntersecting && frame) { frame = null; const again = h("button", { class: "stage-cover btn-reset", type: "button" }, h("img", { src: Evo.media(`${game.pair}/${sel === "r0" ? "p0" : "final"}.webp`), alt: "" }), h("div", {}, h("h2", {}, `${game.title} is paused`), h("span", { class: "btn btn-primary" }, "Play again"))); again.addEventListener("click", run); box.replaceChildren(again); } }, { rootMargin: "300px 0px" }).observe(el); })(); // ---------- headline: one row per generator ---------- $("#headline").replaceChildren(...arms.map((a) => { const p0 = mean(a.games.map((g) => g.reward)), p1 = mean(a.games.map((g) => g.repaired.reward)); const up = a.games.filter((g) => gain(g) > 0).length; return h("div", { class: "head-card" }, h("div", { class: "head-who" }, dot(a.model), h("b", {}, a.model), h("span", {}, `${a.engine} · ${a.games.length} games`)), h("div", { class: "head-nums" }, h("span", { class: "was" }, s100(p0)), h("i", {}, "→"), h("span", {}, s100(p1)), h("em", { class: `delta ${deltaClass(p1 - p0)}` }, signed(p1 - p0))), h("div", { class: "head-sub" }, `mean Overall of the gallery games, round 0 → round 30 · ${up} of ${a.games.length} improved · all 140 tasks under Results`)); })); // ---------- spotlight: one large player and two rows of five thumbnails ---------- const featured = []; // Games whose base scored 0 never built; rising from 0 says little about improvement, so they are left out. const pools = arms.map((a) => a.games.filter((g) => g.reward > 0) .sort((x, y) => gain(y) - gain(x))); const seen = new Set(); while (featured.length < 10 && pools.some((p) => p.length)) { for (const p of pools) { while (p.length) { const g = p.shift(); if (!seen.has(g.task)) { featured.push(g); seen.add(g.task); break; } } } } const spot = $("#spotlight"); const screen = h("div", { class: "spot-screen" }); const info = h("div", { class: "spot-info-slot" }); const rail = h("div", { class: "spot-rail", role: "tablist", "aria-label": "Featured games" }); spot.replaceChildren(h("div", { class: "spot-main" }, h("div", { class: "spot-left" }, screen), info), rail); let spotFrame = null; function showSpot(g, btn) { rail.querySelectorAll("button").forEach((b) => b.setAttribute("aria-selected", String(b === btn))); if (spotFrame) { spotFrame.querySelectorAll("video").forEach(Evo.pause); Evo.unobserve(spotFrame); } spotFrame = Evo.wipeFrame(g.pair, { alt: g.title, eager: true, cls: "frame wipe spot-wipe", after: `Round ${g.repaired.round}` }); Evo.observe(spotFrame); const r = g.repaired, d = gain(g); screen.replaceChildren(spotFrame); info.replaceChildren( h("aside", { class: "spot-info" }, h("div", { class: "m-meta" }, h("span", { class: "pill" }, dot(g.model), g.model), h("span", { class: "pill" }, Evo.genreName(g.genre))), h("h3", {}, g.title), h("p", { class: "spot-blurb" }, g.blurb), h("div", { class: "spot-score" }, h("div", {}, h("span", {}, "Round 0"), h("b", { class: "was" }, s100(g.reward))), h("i", {}, "→"), h("div", {}, h("span", {}, `Round ${r.round}`), h("b", {}, s100(r.reward))), h("em", { class: `delta ${deltaClass(d)}` }, signed(d))), h("div", { class: "spot-cats" }, CATS.map(([k, name]) => h("div", {}, h("span", {}, Evo.SHORT[k]), h("div", { class: "meter meter-pair" }, h("i", { class: "m-final", style: `width:${r[k] * 100}%` }), h("i", { class: "m-p0", style: `left:${g[k] * 100}%` })), h("b", {}, `${i100(g[k])}→${i100(r[k])}`)))), h("button", { class: "btn btn-sm", onclick: () => Evo.openModal(g, featured) }, "Watch larger →"))); } featured.forEach((g, i) => { const b = h("button", { role: "tab", "aria-selected": "false" }, h("img", { src: Evo.media(`${g.pair}/final.webp`), alt: "", loading: "lazy" }), h("span", { class: "rail-title" }, g.title), h("span", { class: "rail-sub" }, dot(g.model), `${s100(g.reward)} → ${s100(g.repaired.reward)}`)); b.addEventListener("click", () => showSpot(g, b)); rail.append(b); if (i === 0) showSpot(g, b); }); // ---------- gallery ---------- function pairCard(g) { const frame = Evo.wipeFrame(g.pair, { alt: g.title, after: `Round ${g.repaired.round}` }); frame.append(h("span", { class: "badge" }, dot(g.model), g.model)); const r = g.repaired, d = gain(g); return h("button", { class: "card pair-card", "aria-label": `${g.title}, ${g.model}, ${s100(g.reward)} to ${s100(r.reward)}` }, frame, h("div", { class: "card-body" }, h("div", { class: "card-genre" }, `${Evo.genreName(g.genre)} · ${g.engine}`), h("h3", {}, g.title), h("div", { class: "pair-score" }, h("span", { class: "was" }, s100(g.reward)), h("i", {}, "→"), h("b", {}, s100(r.reward)), h("em", { class: `delta ${deltaClass(d)}` }, signed(d))))); } Evo.mountGallery($("#gallery-app"), { card: pairCard, pageSize: 18, gridClass: "grid grid-pairs", segment: { key: "model", label: "Generator", options: [["", "All"], ...arms.map((a) => [a.model, a.model])] }, sorts: [ ["gain", "Biggest gain", (a, b) => gain(b) - gain(a)], ["final", "Round 30 score", (a, b) => b.repaired.reward - a.repaired.reward], ["p0", "Weakest start", (a, b) => a.reward - b.reward], ["title", "Title A → Z", (a, b) => a.title.localeCompare(b.title) || a.model.localeCompare(b.model)], ], }); // ---------- results ---------- // Every number in this section is the paper's, not the 181 gallery pairs: // the curves are the four development-scaling panels (20 paired games each), // the rubric chart and the table below are the main results over all 140 tasks. const DEV = {"godot_gpt":{"title":"Godot, Strong Game","n_games":20,"rounds":[0,3,6,9,12,15,18,21,24,27,30],"methods":{"p2c":{"mean":[51.963,51.362,51.991,51.553,48.203,48.849,49.377,43.136,46.265,49.666,51.571],"se":[3.076,2.929,3.022,4.225,3.638,4.005,3.868,3.926,4.671,4.086,3.241],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evo":{"mean":[51.963,62.277,60.87,68.234,62.562,62.95,61.88,67.1,61.209,58.689,68.9],"se":[3.076,5.16,5.971,3.782,6.036,5.045,5.877,4.834,5.625,6.551,3.472],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evov":{"mean":[51.963,62.663,65.044,66.666,67.847,70.254,70.948,70.418,70.699,70.766,71.037],"se":[3.076,3.968,4.128,4.029,3.718,3.532,3.351,3.525,3.535,3.519,3.482],"n":[20,20,20,20,20,20,20,20,20,20,20]}}},"godot_qwen":{"title":"Godot, Weak Game","n_games":20,"rounds":[0,3,6,9,12,15,18,21,24,27,30],"methods":{"p2c":{"mean":[41.737,45.418,45.583,39.049,43.997,41.731,43.477,44.813,46.827,41.487,46.997],"se":[4.342,3.892,3.371,4.246,4.412,4.677,4.335,4.478,3.599,5.74,3.481],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evo":{"mean":[41.737,41.247,51.722,43.804,53.206,52.391,56.102,46.816,50.154,49.303,55.458],"se":[4.342,3.694,3.826,6.101,5.074,5.551,5.755,6.619,4.901,6.897,4.743],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evov":{"mean":[41.737,43.128,48.505,47.012,53.953,55.33,55.663,56.153,56.326,56.667,57.074],"se":[4.342,3.884,4.068,4.66,4.587,4.887,4.589,4.473,4.441,4.507,4.282],"n":[20,20,20,20,20,20,20,20,20,20,20]}}},"phaser_gpt":{"title":"Phaser, Strong Game","n_games":20,"rounds":[0,3,6,9,12,15,18,21,24,27,30],"methods":{"p2c":{"mean":[51.615,55.852,57.392,55.202,55.31,58.777,55.884,55.025,48.147,54.593,52.471],"se":[4.78,4.286,3.225,5.47,4.523,3.386,4.408,4.546,5.753,4.629,5.305],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evo":{"mean":[51.615,54.683,52.405,40.848,48.363,50.791,65.896,58.546,53.622,59.084,54.342],"se":[4.78,5.936,5.636,6.427,5.921,6.148,2.675,5.305,5.007,5.155,5.803],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evov":{"mean":[51.615,54.968,58.201,59.14,59.552,61.545,65.239,66.873,66.873,66.729,67.287],"se":[4.78,4.993,4.28,4.294,4.229,4.047,2.569,2.509,2.509,2.458,2.493],"n":[20,19,19,19,19,19,19,19,19,19,19]}}},"phaser_qwen":{"title":"Phaser, Weak Game","n_games":20,"rounds":[0,3,6,9,12,15,18,21,24,27,30],"methods":{"p2c":{"mean":[46.197,51.647,55.425,52.089,54.278,53.012,52.432,53.496,56.008,50.056,56.153],"se":[5.613,4.594,3.628,4.632,4.767,5.679,5.695,5.006,4.877,5.334,3.709],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evo":{"mean":[46.197,42.45,50.269,52.772,48.081,55.135,48.356,49.88,52.949,49.121,53.238],"se":[5.613,5.874,4.561,5.627,5.809,4.297,5.753,6.079,5.939,6.223,5.861],"n":[20,20,20,20,20,20,20,20,20,20,20]},"evov":{"mean":[46.197,54.58,54.814,54.968,55.388,55.388,56.418,57.167,57.429,57.479,57.479],"se":[5.613,3.806,3.889,3.824,3.764,3.764,3.646,3.706,3.727,3.738,3.738],"n":[20,20,20,20,20,20,20,20,20,20,20]}}}}; const DEVPANELS = [ ["godot_gpt", "Godot · GPT-5.5", "strong base"], ["godot_qwen", "Godot · Qwen3.8-27B", "weak base"], ["phaser_gpt", "Phaser · GPT-5.5", "strong base"], ["phaser_qwen", "Phaser · Qwen3.8-27B", "weak base"], ]; const DEVM = [ { k: "p2c", label: "Play2Code", color: "#74849f", dash: "1.6 2.6", w: 2 }, { k: "evo", label: "RSIGame (w/o Global Quality Monitor)", color: "#6f8fff", dash: "6 3", w: 2.2 }, { k: "evov", label: "RSIGame (w/ Global Quality Monitor)", color: "#34e3f0", dash: "", w: 2.6 }, ]; const PAPER = { godot: { caption: "Main results on GameCraft-Bench (Godot), all 140 tasks", groups: [ { gen: "Codex + GPT-5.5 (high)", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [58.9, 51.7, 51.4, 44.6, 50.26, "0.26M", null] }, { name: "+ Play2Code", kind: "p2c", v: [59.2, 51.3, 52.2, 45.8, 50.74, "3.20M", "$1.06"] }, { name: "+ RSIGame", kind: "evo", v: [72.8, 60.3, 65.9, 64.5, 64.53, "3.12M", "$0.88"] }] }, { gen: "Kimi-K2.6", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [40.2, 30.3, 35.4, 21.9, 29.63, "3.22M", null] }, { name: "+ Play2Code", kind: "p2c", v: [44.8, 35.4, 40.2, 28.6, 35.19, "7.07M", "$1.27"] }, { name: "+ RSIGame", kind: "evo", v: [51.7, 40.1, 47.1, 45.4, 44.77, "7.39M", "$1.18"] }] }, { gen: "GLM-5.3-Flash", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [35.7, 30.2, 32.0, 24.9, 29.46, "0.44M", null] }, { name: "+ Play2Code", kind: "p2c", v: [47.2, 39.7, 41.1, 32.7, 38.59, "3.18M", "$0.99"] }, { name: "+ RSIGame", kind: "evo", v: [51.9, 44.4, 49.9, 53.9, 49.72, "5.06M", "$1.25"] }] }, { gen: "Qwen3.8-27B", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [41.2, 33.4, 38.6, 38.3, 37.07, "6.41M", null] }, { name: "+ Play2Code", kind: "p2c", v: [44.6, 36.1, 42.1, 42.5, 40.53, "9.92M", "$1.32"] }, { name: "+ RSIGame", kind: "evo", v: [51.2, 40.4, 47.5, 53.8, 47.77, "9.13M", "$1.09"] }] }, { gen: "Qwen3.8-27B (SFT)", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [56.1, 47.5, 49.8, 44.8, 48.22, "0.57M", null] }, { name: "+ Play2Code", kind: "p2c", v: [57.4, 48.1, 51.6, 47.2, 49.71, "3.74M", "$1.08"] }, { name: "+ RSIGame", kind: "evo", v: [68.7, 55.8, 62.8, 63.2, 61.38, "3.49M", "$0.94"] }] }, ], note: "Every score is the mean of three replay-and-score runs over all 140 tasks with the Qwen3.8-27B judge. Rows group by the generator of the frozen base P₀; every development method gets an identical clone, the same repair backbone and the same 30-round budget. Tok. = mean billable tokens per task (input excluding cache reads, plus output); Cost = mean development spend per task. The Qwen3.8-27B (SFT) group is the same generator after its development experience is internalized by fine-tuning.", }, phaser: { caption: "Same tasks, rubric and protocol under Phaser — only the engine changes", groups: [ { gen: "OpenGame + GPT-5.5", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [49.1, 38.6, 53.6, 58.6, 49.44, "5.36M", null] }, { name: "+ Play2Code", kind: "p2c", v: [57.1, 42.0, 60.0, 65.2, 55.10, "8.34M", "$1.43"] }, { name: "+ RSIGame", kind: "evo", v: [59.8, 44.2, 62.4, 69.7, 58.21, "8.38M", "$1.44"] }] }, { gen: "Qwen3.8-27B", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [40.6, 29.5, 43.7, 48.7, 40.03, "9.03M", null] }, { name: "+ Play2Code", kind: "p2c", v: [51.0, 36.4, 53.2, 58.0, 48.70, "12.99M", "$1.75"] }, { name: "+ RSIGame", kind: "evo", v: [52.0, 36.5, 54.6, 61.3, 50.24, "12.78M", "$1.50"] }] }, { gen: "Qwen3.8-27B (SFT)", rows: [ { name: "Base (frozen P₀)", kind: "base", v: [47.1, 35.4, 48.9, 52.4, 45.14, "5.42M", null] }, { name: "+ Play2Code", kind: "p2c", v: [55.6, 41.7, 57.3, 61.8, 53.15, "9.58M", "$1.61"] }, { name: "+ RSIGame", kind: "evo", v: [60.4, 46.2, 62.3, 68.4, 58.53, "9.31M", "$1.53"] }] }, ], note: "Identical tasks, pipeline, rubric and protocol as the Godot table; only the engine differs. Every row covers all 140 tasks, with closed token and cost ledgers.", }, }; const ARMCOL = { "GPT-5.5": "gpt", "Kimi-K2.6": "kimi", "GLM-5.3-Flash": "glm", "Qwen3.8-27B": "qwen", "Qwen3.8-27B (SFT)": "sft" }; const lineSwatch = (m) => { const S = Evo.svg, s = S("svg", { viewBox: "0 0 26 8", class: "lg-line", "aria-hidden": "true" }); s.append(S("line", { x1: 1, x2: 25, y1: 4, y2: 4, stroke: m.color, "stroke-width": m.w + 0.4, "stroke-dasharray": m.dash, "stroke-linecap": "round" })); return s; }; $("#dev-legend").replaceChildren(...DEVM.slice().reverse().map((m) => h("span", {}, lineSwatch(m), m.label))); $("#cat-legend").replaceChildren( ...Object.entries(ARMCOL).map(([name, k]) => h("span", {}, h("i", { class: `dot ${k}` }), name))); (function devScaling() { const W = 336, H = 206, L = 36, R = 12, T = 12, B = 30; const panels = DEVPANELS.map(([key, title, sub], pi) => { const bottom = pi >= DEVPANELS.length - 2; // the four panels share one x axis; label only the bottom row const d = DEV[key], rounds = d.rounds; const drawn = DEVM.filter((m) => d.methods[m.k]); const vals = drawn.flatMap((m) => d.methods[m.k].mean.flatMap((v, i) => [v - d.methods[m.k].se[i], v + d.methods[m.k].se[i]])); const step = Math.max(...vals) - Math.min(...vals) > 34 ? 10 : 5; const lo = Math.floor((Math.min(...vals) - 1) / step) * step, hi = Math.ceil((Math.max(...vals) + 1) / step) * step; const x = (r) => L + (r / 30) * (W - L - R), y = (v) => T + (1 - (v - lo) / (hi - lo)) * (H - T - B); const S = Evo.svg; const svg = S("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": `${title}: mean Overall score after a development budget of k rounds, three methods` }); // only our line glows; the other two stay plain so the contrast reads svg.append(S("defs", {}, S("filter", { id: `glow-${key}`, x: "-30%", y: "-70%", width: "160%", height: "240%" }, S("feDropShadow", { dx: 0, dy: 0, stdDeviation: 3, "flood-color": "#34e3f0", "flood-opacity": "0.55" })))); for (let t = lo; t <= hi + 1e-9; t += step) { svg.append(S("line", { class: "grid-line", x1: L, x2: W - R, y1: y(t), y2: y(t) }), S("text", { class: "val-label", x: L - 7, y: y(t) + 4, "text-anchor": "end" }, String(t))); } const axis = S("g", { class: "axis" }); [0, 6, 12, 18, 24, 30].forEach((r) => axis.append( S("text", { x: x(r), y: H - 12, "text-anchor": "middle" }, r === 0 ? "P0" : String(r)))); if (bottom) axis.append(S("text", { x: (L + W - R) / 2, y: H - 1, "text-anchor": "middle", class: "ax-title" }, "development budget (rounds)")); svg.append(axis); const cross = S("line", { class: "crosshair", x1: 0, x2: 0, y1: T, y2: H - B, visibility: "hidden" }); svg.append(cross); drawn.forEach((m) => { const s = d.methods[m.k]; const up = rounds.map((r, i) => `${x(r)},${y(s.mean[i] + s.se[i])}`); const dn = rounds.map((r, i) => `${x(r)},${y(s.mean[i] - s.se[i])}`).reverse(); svg.append(S("path", { d: `M${up.join("L")}L${dn.join("L")}Z`, fill: m.color, opacity: 0.11 })); }); drawn.forEach((m) => { const s = d.methods[m.k]; const ours = m.k === "evov"; const path = S("path", { d: rounds.map((r, i) => `${i ? "L" : "M"}${x(r)},${y(s.mean[i])}`).join(""), fill: "none", stroke: m.color, "stroke-width": m.w, "stroke-dasharray": m.dash, "stroke-linecap": "round", "stroke-linejoin": "round" }); if (ours) path.setAttribute("filter", `url(#glow-${key})`); svg.append(path); svg.append(S("circle", { cx: x(30), cy: y(s.mean[10]), r: ours ? 4 : 3.4, fill: m.color, stroke: "var(--surface)", "stroke-width": 1.6, class: ours ? "pulse-dot" : "" })); }); const hit = S("rect", { class: "hit", x: L, y: T, width: W - L - R, height: H - T - B, fill: "transparent" }); hit.addEventListener("mousemove", (e) => { const box = svg.getBoundingClientRect(); const px = ((e.clientX - box.left) / box.width) * W; const r = rounds.reduce((best, c) => (Math.abs(x(c) - px) < Math.abs(x(best) - px) ? c : best), rounds[0]); const i = rounds.indexOf(r); cross.setAttribute("x1", x(r)); cross.setAttribute("x2", x(r)); cross.setAttribute("visibility", "visible"); Evo.showTip(e, `${Evo.esc(title)} · ${r === 0 ? "frozen base P0" : `budget ${r} rounds`}` + drawn.slice().reverse().map((m) => `
${Evo.esc(m.label)} ${d.methods[m.k].mean[i].toFixed(1)}`).join("")); }); hit.addEventListener("mouseleave", () => { cross.setAttribute("visibility", "hidden"); Evo.hideTip(); }); svg.append(hit); const gains = h("div", { class: "dev-gains" }, ...drawn.slice().reverse().map((m) => { const s = d.methods[m.k], g = s.mean[10] - s.mean[0]; return h("span", {}, h("i", { class: "dot", style: `background:${m.color}` }), h("b", { class: g > 0 ? "up" : "down" }, `${g > 0 ? "+" : "−"}${Math.abs(g).toFixed(1)}`)); })); return h("figure", { class: "dev-panel" }, h("figcaption", {}, h("b", {}, title), h("span", {}, `${sub} · n=${d.n_games}`)), svg, gains); }); $("#devscale").replaceChildren(...panels); })(); (function categoryDumbbells() { // All 140 tasks: Base (hollow) -> RSIGame (filled), the two rows of the main table. const rows = PAPER.godot.groups .filter((gp) => gp.rows.some((r) => r.kind === "evo" && r.v[0] != null)) .map((gp) => ({ model: gp.gen.replace(/^Codex \+ /, "").replace(/ \(high\)$/, ""), base: gp.rows.find((r) => r.kind === "base").v, evo: gp.rows.find((r) => r.kind === "evo").v, })); const W = 460, L = 92, R = 86, ROW = 34, GROUP = 30, T = 12, B = 30; const H = T + CATS.length * (rows.length * ROW + GROUP) + B; const x = (v) => L + (v / 100) * (W - L - R); const S = Evo.svg, svg = S("svg", { viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": "Mean rubric category score over all 140 Godot tasks, frozen base to RSIGame, per generator" }); [0, 25, 50, 75, 100].forEach((t) => svg.append(S("line", { class: "grid-line", x1: x(t), x2: x(t), y1: T, y2: H - B }), S("text", { class: "val-label", x: x(t), y: H - 8, "text-anchor": "middle" }, String(t)))); CATS.forEach(([k], ci) => { const y0 = T + ci * (rows.length * ROW + GROUP); svg.append(S("text", { class: "row-label", x: 0, y: y0 + (rows.length * ROW) / 2 + 4 }, Evo.SHORT[k])); rows.forEach((a, ai) => { const v0 = a.base[ci], v1 = a.evo[ci]; const y = y0 + ai * ROW + ROW / 2, color = `var(--${ARMCOL[a.model]})`; svg.append(S("line", { x1: x(v0), x2: x(v1), y1: y, y2: y, stroke: color, "stroke-width": 2.4 }), S("circle", { cx: x(v0), cy: y, r: 4.5, fill: "var(--surface)", stroke: color, "stroke-width": 2 }), S("circle", { cx: x(v1), cy: y, r: 10, fill: color, opacity: 0.16 }), S("circle", { cx: x(v1), cy: y, r: 5.2, fill: color, class: "strip-dot" }), S("text", { class: "val-label pair-val", x: W - R + 8, y: y + 4 }, `${v0.toFixed(1)} \u2192 ${v1.toFixed(1)}`)); const hit = S("rect", { class: "hit", x: L, y: y - ROW / 2, width: W - L, height: ROW, fill: "transparent" }); hit.addEventListener("mousemove", (e) => Evo.showTip(e, `${Evo.esc(CATS[ci][1])} · ${Evo.esc(a.model)} · 140 tasks
frozen base ${v0.toFixed(1)} → RSIGame ${v1.toFixed(1)} ${v1 - v0 > 0 ? "+" : "−"}${Math.abs(v1 - v0).toFixed(1)}`)); hit.addEventListener("mouseleave", Evo.hideTip); svg.append(hit); }); }); $("#cats").replaceChildren(svg); })(); (function mainTable() { const head = ["Method", "Mechanics", "Depth", "Visuals", "Art", "Overall ↑", "Tok. ↓", "Cost ↓"]; let cur = "godot"; function draw() { const T = PAPER[cur], body = []; T.groups.forEach((gp) => { body.push(h("tr", { class: "grp" }, h("th", { colspan: 8, scope: "colgroup" }, `Generator: ${gp.gen}`))); gp.rows.forEach((r) => body.push(h("tr", { class: r.kind === "evo" ? "ours" : "" }, h("th", { scope: "row" }, r.name, r.note ? h("sup", {}, r.note) : null), ...r.v.map((v, ci) => h("td", { class: v == null ? "na" : "" }, v == null ? "—" : (typeof v === "number" ? v.toFixed(ci === 4 ? 2 : 1) : v)))))); }); $("#summary").replaceChildren( h("caption", {}, T.caption), h("thead", {}, h("tr", {}, head.map((c) => h("th", { scope: "col" }, c)))), h("tbody", {}, body)); $("#judge-note").textContent = T.note; $("#tbl-seg").querySelectorAll("button").forEach((b) => b.classList.toggle("on", b.dataset.k === cur)); } $("#tbl-seg").replaceChildren(...[["godot", "Godot"], ["phaser", "Phaser"]].map(([k, t]) => h("button", { type: "button", "data-k": k, onclick: () => { cur = k; draw(); } }, t))); draw(); })(); Evo.wireCopy($("#copy-bib"), $("#bib")); }, { dataset: "evo.json", simple: true });