/* 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 });