← back to Model Arena
complete and verify all five arena challenges
fc408ceb63efc7b26b393aaf2da2151c5a3b7987 · 2026-09-01 12:35:51 -0700 · Steve Abrams
Files touched
A data/artifacts/89e23514bd71/claude-code.htmlA data/artifacts/89e23514bd71/claude-code.pngM data/challenges.jsonM verification/e2e-proof.jsonM verification/five-detailed-live.pngM verification/five-detailed-proof.jsonM verification/five-detailed.e2e.js
Diff
commit fc408ceb63efc7b26b393aaf2da2151c5a3b7987
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Sep 1 12:35:51 2026 -0700
complete and verify all five arena challenges
---
data/artifacts/89e23514bd71/claude-code.html | 929 +++++++++++++++++++++++++++
data/artifacts/89e23514bd71/claude-code.png | Bin 0 -> 125767 bytes
data/challenges.json | 98 ++-
verification/e2e-proof.json | 10 +-
verification/five-detailed-live.png | Bin 235640 -> 238178 bytes
verification/five-detailed-proof.json | 9 +-
verification/five-detailed.e2e.js | 7 +-
7 files changed, 1011 insertions(+), 42 deletions(-)
diff --git a/data/artifacts/89e23514bd71/claude-code.html b/data/artifacts/89e23514bd71/claude-code.html
new file mode 100644
index 0000000..97799ba
--- /dev/null
+++ b/data/artifacts/89e23514bd71/claude-code.html
@@ -0,0 +1,929 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Storm Relay — Orbital Freight Command</title>
+<style>
+ :root{
+ --bg:#0e1826; --panel:#16233a; --panel2:#1d2f4a; --line:#2a3c5a;
+ --ink:#e8eef7; --muted:#93a4bd; --amber:#f4b860; --teal:#5fd0c5;
+ --green:#8fd694; --red:#e88a8a; --violet:#b79ce8; --blue:#6db3e8;
+ --shadow:0 8px 30px rgba(0,0,0,.35);
+ --r:12px;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;padding:0}
+ body{
+ font-family:"Segoe UI",system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
+ background:radial-gradient(1200px 700px at 70% -10%, #17243b 0%, var(--bg) 60%);
+ color:var(--ink); min-height:100vh; line-height:1.45;
+ -webkit-font-smoothing:antialiased;
+ }
+ .reduce *{transition:none!important; animation:none!important}
+ h1,h2,h3{margin:0 0 .3em; font-weight:600; letter-spacing:.3px}
+ button{font-family:inherit; cursor:pointer}
+ button:focus-visible, select:focus-visible, input:focus-visible, [tabindex]:focus-visible{
+ outline:3px solid var(--teal); outline-offset:2px; border-radius:6px;
+ }
+ .btn{
+ background:var(--panel2); color:var(--ink); border:1px solid var(--line);
+ padding:9px 14px; border-radius:8px; font-size:14px; transition:.15s;
+ }
+ .btn:hover{background:#26406a; border-color:var(--teal)}
+ .btn.primary{background:linear-gradient(180deg,#f4c576,#e0a24a); color:#20140a; border:none; font-weight:600}
+ .btn.primary:hover{filter:brightness(1.07)}
+ .btn.ghost{background:transparent}
+ .btn:disabled{opacity:.4; cursor:not-allowed}
+ .tag{display:inline-block; font-size:11px; padding:1px 7px; border-radius:20px; border:1px solid var(--line); color:var(--muted)}
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}
+
+ /* Screens */
+ .screen{display:none; max-width:1180px; margin:0 auto; padding:16px}
+ .screen.active{display:block}
+
+ /* Setup */
+ #setup{max-width:720px; text-align:center; padding-top:6vh}
+ .logo{font-size:13px; letter-spacing:4px; color:var(--teal); text-transform:uppercase}
+ #setup h1{font-size:40px; margin-top:6px; background:linear-gradient(90deg,#f4b860,#5fd0c5); -webkit-background-clip:text; background-clip:text; color:transparent}
+ .lede{color:var(--muted); max-width:560px; margin:0 auto 22px; font-size:15px}
+ .diffs{display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin:14px 0}
+ .diff{
+ background:var(--panel); border:2px solid var(--line); border-radius:var(--r);
+ padding:14px 16px; width:200px; text-align:left; transition:.15s;
+ }
+ .diff[aria-pressed="true"]{border-color:var(--amber); background:var(--panel2)}
+ .diff b{display:block; margin-bottom:4px}
+ .diff small{color:var(--muted); font-size:12px}
+ .opts{display:flex; gap:18px; justify-content:center; margin:18px 0; flex-wrap:wrap}
+ .opts label{display:flex; align-items:center; gap:7px; font-size:14px; color:var(--muted)}
+ .startrow{display:flex; gap:10px; justify-content:center; margin-top:6px; flex-wrap:wrap}
+
+ /* Game layout */
+ .topbar{
+ display:flex; align-items:center; gap:14px; flex-wrap:wrap;
+ background:var(--panel); border:1px solid var(--line); border-radius:var(--r);
+ padding:10px 14px; margin-bottom:12px; box-shadow:var(--shadow);
+ }
+ .topbar .stat{display:flex; flex-direction:column; line-height:1.1}
+ .topbar .stat b{font-size:18px}
+ .topbar .stat span{font-size:11px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px}
+ .topbar .grow{flex:1}
+ .fuelbar{height:8px; width:120px; background:#0c1522; border-radius:6px; overflow:hidden; margin-top:4px}
+ .fuelbar i{display:block; height:100%; background:linear-gradient(90deg,var(--teal),var(--amber))}
+
+ .grid{display:grid; grid-template-columns:1.5fr 1fr; gap:12px}
+ .panel{background:var(--panel); border:1px solid var(--line); border-radius:var(--r); padding:14px; box-shadow:var(--shadow)}
+ .panel h3{font-size:13px; text-transform:uppercase; letter-spacing:1px; color:var(--teal); display:flex; align-items:center; gap:8px}
+ .panel h3 .hint{margin-left:auto; font-size:11px; color:var(--muted); text-transform:none; letter-spacing:0}
+
+ /* Timeline */
+ .timeline{display:flex; gap:4px; margin-top:8px; flex-wrap:wrap}
+ .tick{flex:1; min-width:20px; text-align:center; padding:6px 2px; border-radius:7px; background:var(--panel2); border:1px solid var(--line); font-size:12px; position:relative}
+ .tick.now{background:var(--amber); color:#20140a; font-weight:700; border-color:var(--amber)}
+ .tick.past{opacity:.45}
+ .tick .ev{position:absolute; top:-5px; right:-3px; width:8px; height:8px; border-radius:50%}
+ .storm{margin-top:8px; font-size:12px; color:var(--muted)}
+ .storm b{color:var(--red)}
+
+ /* Map */
+ #mapwrap{position:relative}
+ svg#map{width:100%; height:auto; display:block; background:radial-gradient(600px 400px at 50% 30%, #12243d, #0d1727); border-radius:10px; border:1px solid var(--line)}
+ .node{cursor:pointer}
+ .node circle.core{stroke:#0c1522; stroke-width:2}
+ .node.sel circle.ring{stroke:var(--teal); stroke-width:2.5}
+ .node.closed{opacity:.35}
+ .nlabel{font-size:12px; fill:var(--ink); font-weight:600}
+ .nsub{font-size:9.5px; fill:var(--muted)}
+ .lane{stroke:#24374f; stroke-width:1; stroke-dasharray:3 5}
+ .lane.closed{stroke:#3a2530; stroke-dasharray:2 8}
+ .routeline{stroke-width:2.5; opacity:.8; fill:none}
+ .warn{fill:var(--red); font-size:10px; font-weight:700}
+
+ /* Settlement cards */
+ .cards{display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:8px; margin-top:8px}
+ .scard{background:var(--panel2); border:1px solid var(--line); border-radius:9px; padding:9px; font-size:12px; cursor:pointer; transition:.15s}
+ .scard:hover{border-color:var(--teal)}
+ .scard.sel{border-color:var(--amber)}
+ .scard .nm{font-weight:600; font-size:13px; display:flex; align-items:center; gap:6px}
+ .dot{width:9px;height:9px;border-radius:50%; flex:none}
+ .trust{height:6px; background:#0c1522; border-radius:4px; overflow:hidden; margin:5px 0}
+ .trust i{display:block;height:100%}
+ .need{display:flex; justify-content:space-between; color:var(--muted); font-size:11px}
+ .need b{color:var(--ink)}
+ .need.alert b{color:var(--red)}
+
+ /* Planner */
+ .field{margin:9px 0}
+ .field label{display:block; font-size:11px; text-transform:uppercase; letter-spacing:.5px; color:var(--muted); margin-bottom:4px}
+ select, input[type=number]{
+ width:100%; background:#0f1c2f; color:var(--ink); border:1px solid var(--line);
+ border-radius:7px; padding:8px; font-size:14px; font-family:inherit;
+ }
+ .row2{display:grid; grid-template-columns:1fr 1fr; gap:8px}
+ .ships{display:flex; gap:6px}
+ .ships label{flex:1; background:#0f1c2f; border:1px solid var(--line); border-radius:7px; padding:7px 4px; text-align:center; font-size:11px; cursor:pointer}
+ .ships input{position:absolute; opacity:0; width:0}
+ .ships label.on{border-color:var(--amber); background:var(--panel2)}
+ .ships b{display:block; font-size:12px}
+ .est{font-size:12px; color:var(--muted); margin:8px 0}
+ .est b{color:var(--amber)}
+ .plist{margin-top:10px; max-height:160px; overflow:auto}
+ .prow{display:flex; align-items:center; gap:6px; background:var(--panel2); border:1px solid var(--line); border-radius:7px; padding:6px 8px; margin-bottom:6px; font-size:12px}
+ .prow .x{margin-left:auto; background:none; border:none; color:var(--red); font-size:16px; padding:0 4px}
+ .empty{color:var(--muted); font-size:12px; text-align:center; padding:10px}
+
+ /* Log */
+ #log{max-height:230px; overflow:auto; font-size:12.5px}
+ .lrow{padding:6px 8px; border-left:3px solid var(--line); margin-bottom:5px; background:var(--panel2); border-radius:0 6px 6px 0}
+ .lrow.good{border-color:var(--green)}
+ .lrow.bad{border-color:var(--red)}
+ .lrow.warn{border-color:var(--amber)}
+ .lrow .t{color:var(--muted); font-size:10.5px}
+
+ /* Detail */
+ #detail .drow{display:flex; justify-content:space-between; padding:5px 0; border-bottom:1px solid var(--line); font-size:13px}
+ #detail .drow span{color:var(--muted)}
+ .resline{display:flex; align-items:center; gap:8px; margin:6px 0}
+ .resbar{flex:1; height:16px; background:#0c1522; border-radius:5px; overflow:hidden; position:relative}
+ .resbar i{display:block; height:100%}
+ .resbar b{position:absolute; right:6px; top:0; font-size:11px; line-height:16px}
+
+ /* Overlays */
+ .overlay{position:fixed; inset:0; background:rgba(6,12,22,.82); display:none; align-items:center; justify-content:center; padding:16px; z-index:50}
+ .overlay.active{display:flex}
+ .modal{background:var(--panel); border:1px solid var(--line); border-radius:16px; max-width:520px; width:100%; padding:22px; box-shadow:var(--shadow)}
+ .modal h2{font-size:22px}
+ .modal p{color:var(--muted); font-size:14px}
+ .choices{display:flex; flex-direction:column; gap:10px; margin-top:14px}
+ .choice{text-align:left; padding:12px 14px; border-radius:10px; border:1px solid var(--line); background:var(--panel2)}
+ .choice:hover{border-color:var(--amber)}
+ .choice b{display:block; margin-bottom:3px}
+ .choice small{color:var(--muted)}
+ .tutmap{display:flex; gap:6px; justify-content:center; margin:14px 0}
+ .tutmap i{width:8px;height:8px;border-radius:50%; background:var(--line)}
+ .tutmap i.on{background:var(--amber)}
+
+ /* Summary */
+ #summary .sumhead{text-align:center; padding:10px 0 4px}
+ #summary .verdict{font-size:34px; font-weight:700}
+ .win .verdict{color:var(--green)} .lose .verdict{color:var(--red)}
+ .sumgrid{display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:14px}
+ .decisive li{margin-bottom:8px; color:var(--muted); font-size:13.5px}
+ .decisive li b{color:var(--ink)}
+ .sfinal{display:flex; justify-content:space-between; padding:6px 0; border-bottom:1px solid var(--line); font-size:13px}
+
+ @media (max-width:820px){
+ .grid{grid-template-columns:1fr}
+ }
+ @media (max-width:420px){
+ .screen{padding:10px}
+ #setup h1{font-size:30px}
+ .diff{width:100%}
+ .topbar .stat b{font-size:16px}
+ .cards{grid-template-columns:1fr 1fr}
+ .row2{grid-template-columns:1fr}
+ }
+</style>
+</head>
+<body>
+
+<!-- ================= SETUP ================= -->
+<section id="setup" class="screen active">
+ <div class="logo">Cooperative Orbital Logistics</div>
+ <h1>Storm Relay</h1>
+ <p class="lede">A solar storm will seal the transfer windows in twelve turns. Five settlements keep each other alive by trading water, medicine, reactor parts, and research samples. Route your ships well — everyone survives together, or no one does.</p>
+
+ <div class="diffs" role="group" aria-label="Difficulty">
+ <button class="diff" data-diff="relaxed" aria-pressed="false"><b>Relaxed</b><small>44 fuel · gentle trust loss · learn the flow</small></button>
+ <button class="diff" data-diff="standard" aria-pressed="true"><b>Standard</b><small>32 fuel · balanced pressure</small></button>
+ <button class="diff" data-diff="tense" aria-pressed="false"><b>Tense</b><small>24 fuel · steep collapse · for veterans</small></button>
+ </div>
+
+ <div class="opts">
+ <label><input type="checkbox" id="optSound"> Sound effects</label>
+ <label><input type="checkbox" id="optMotion"> Reduced motion</label>
+ </div>
+
+ <div class="startrow">
+ <button class="btn primary" id="startBtn">Begin Mission</button>
+ <button class="btn" id="tutBtn">How to play</button>
+ </div>
+ <p class="lede" style="margin-top:22px; font-size:12px">Keyboard: <b>Tab</b> to move · <b>Enter/Space</b> to activate · <b>E</b> end turn · <b>?</b> pause menu. Sound is off by default.</p>
+</section>
+
+<!-- ================= GAME ================= -->
+<section id="game" class="screen">
+ <div class="topbar">
+ <div class="stat"><b id="turnNum">1<span style="font-size:12px;color:var(--muted)">/12</span></b><span>Turn</span></div>
+ <div class="stat">
+ <b id="fuelNum">32</b><span>Fuel remaining</span>
+ <div class="fuelbar"><i id="fuelFill" style="width:100%"></i></div>
+ </div>
+ <div class="stat"><b id="coopNum">100%</b><span>Cooperative health</span></div>
+ <div class="grow"></div>
+ <button class="btn" id="tutBtn2" title="Tutorial">?</button>
+ <button class="btn" id="pauseBtn" title="Pause menu">Menu</button>
+ <button class="btn primary" id="endBtn">End Turn ▸</button>
+ </div>
+
+ <div class="grid">
+ <!-- LEFT: map + settlement cards -->
+ <div>
+ <div class="panel" id="mapwrap">
+ <h3>Star Map <span class="hint">click a settlement to inspect</span></h3>
+ <svg id="map" viewBox="0 0 600 400" role="img" aria-label="Star map of five settlements and transfer lanes"></svg>
+ </div>
+ <div class="panel" style="margin-top:12px">
+ <h3>Settlements <span class="hint">need · turns of supply left</span></h3>
+ <div class="cards" id="cards"></div>
+ </div>
+ </div>
+
+ <!-- RIGHT: timeline, planner, detail, log -->
+ <div>
+ <div class="panel">
+ <h3>Storm Timeline</h3>
+ <div class="timeline" id="timeline"></div>
+ <div class="storm" id="stormNote"></div>
+ </div>
+
+ <div class="panel" style="margin-top:12px">
+ <h3>Cargo Planner <span class="hint">revise freely before ending turn</span></h3>
+ <div class="field">
+ <label>Ship</label>
+ <div class="ships" id="ships"></div>
+ </div>
+ <div class="row2">
+ <div class="field"><label for="fromSel">From</label><select id="fromSel"></select></div>
+ <div class="field"><label for="toSel">To</label><select id="toSel"></select></div>
+ </div>
+ <div class="row2">
+ <div class="field"><label for="resSel">Cargo</label><select id="resSel"></select></div>
+ <div class="field"><label for="amtInp">Amount</label><input type="number" id="amtInp" min="1" value="1"></div>
+ </div>
+ <div class="est" id="estLine"></div>
+ <button class="btn primary" id="addBtn" style="width:100%">Add route to queue</button>
+ <div class="plist" id="plist"></div>
+ </div>
+
+ <div class="panel" style="margin-top:12px">
+ <h3 id="detailTitle">Settlement Detail</h3>
+ <div id="detail"><p class="empty">Select a settlement on the map or cards.</p></div>
+ </div>
+
+ <div class="panel" style="margin-top:12px">
+ <h3>Event Log</h3>
+ <div id="log" aria-live="polite"></div>
+ </div>
+ </div>
+ </div>
+</section>
+
+<!-- ================= SUMMARY ================= -->
+<section id="summary" class="screen">
+ <div class="panel">
+ <div class="sumhead" id="sumhead">
+ <div class="logo">Mission Debrief</div>
+ <div class="verdict" id="verdict">—</div>
+ <p class="lede" id="verdictSub"></p>
+ </div>
+ <div class="sumgrid">
+ <div>
+ <h3 style="color:var(--teal); font-size:13px; text-transform:uppercase; letter-spacing:1px">Final Standing</h3>
+ <div id="finalList"></div>
+ </div>
+ <div>
+ <h3 style="color:var(--teal); font-size:13px; text-transform:uppercase; letter-spacing:1px">Decisive Choices</h3>
+ <ul class="decisive" id="decisive"></ul>
+ </div>
+ </div>
+ <div class="startrow" style="margin-top:18px">
+ <button class="btn primary" id="againBtn">New Mission</button>
+ </div>
+ </div>
+</section>
+
+<!-- Overlays -->
+<div class="overlay" id="tutOverlay"><div class="modal">
+ <div class="tutmap" id="tutDots"></div>
+ <h2 id="tutTitle"></h2>
+ <p id="tutBody"></p>
+ <div class="startrow" style="justify-content:space-between; margin-top:16px">
+ <button class="btn ghost" id="tutBack">Back</button>
+ <button class="btn primary" id="tutNext">Next</button>
+ </div>
+</div></div>
+
+<div class="overlay" id="negOverlay"><div class="modal">
+ <span class="tag" style="border-color:var(--violet); color:var(--violet)">Negotiation</span>
+ <h2 id="negTitle"></h2>
+ <p id="negBody"></p>
+ <div class="choices" id="negChoices"></div>
+</div></div>
+
+<div class="overlay" id="pauseOverlay"><div class="modal">
+ <h2>Mission Paused</h2>
+ <p>Take your time — the storm waits for your command.</p>
+ <div class="choices">
+ <button class="choice" id="resumeBtn"><b>Resume</b><small>Return to the bridge</small></button>
+ <button class="choice" id="tutBtn3"><b>Replay tutorial</b><small>Walk through the interface again</small></button>
+ <button class="choice" id="restartBtn"><b>Restart / change difficulty</b><small>Reset to mission setup</small></button>
+ </div>
+ <div class="opts" style="margin-top:14px; justify-content:flex-start">
+ <label><input type="checkbox" id="optSound2"> Sound</label>
+ <label><input type="checkbox" id="optMotion2"> Reduced motion</label>
+ </div>
+</div></div>
+
+<script>
+"use strict";
+/* ---------- Static config ---------- */
+const RES = [
+ {k:'water', n:'Water', c:'#6db3e8'},
+ {k:'medicine', n:'Medicine', c:'#8fd694'},
+ {k:'parts', n:'Reactor Parts', c:'#f4b860'},
+ {k:'samples', n:'Research Samples',c:'#b79ce8'}
+];
+const RC = Object.fromEntries(RES.map(r=>[r.k,r]));
+const SHIPS = [
+ {k:'courier', n:'Courier', cap:4, fuel:1},
+ {k:'hauler', n:'Hauler', cap:10, fuel:3},
+ {k:'freighter',n:'Freighter', cap:20, fuel:6}
+];
+const SC = Object.fromEntries(SHIPS.map(s=>[s.k,s]));
+
+// Settlement blueprints. produces/consumes = per-turn. Positions on 600x400 map.
+const BLUEPRINT = [
+ {id:'HEL', name:'Helios Anchor', c:'#f4b860', x:300, y:70,
+ produces:{parts:3}, consumes:{water:3},
+ stock:{water:12, medicine:2, parts:8, samples:1},
+ role:'Fusion drydock. Builds reactor parts; thirsty for water.'},
+ {id:'VRD', name:'Verdant Ring', c:'#8fd694', x:110, y:165,
+ produces:{water:5}, consumes:{medicine:2},
+ stock:{water:16, medicine:8, parts:1, samples:0},
+ role:'Hydroponic ring. Exports water; its crews need medicine.'},
+ {id:'CND', name:'Cinder Foundry', c:'#e8a06e', x:490, y:165,
+ produces:{parts:4}, consumes:{samples:2},
+ stock:{water:2, medicine:1, parts:12, samples:8},
+ role:'Ore foundry. Casts reactor parts using research samples.'},
+ {id:'KES', name:'Kestrel Haven', c:'#5fd0c5', x:180, y:345,
+ produces:{medicine:4}, consumes:{parts:2},
+ stock:{water:3, medicine:16, parts:8, samples:1},
+ role:'Med colony. Brews medicine; its labs run on reactor parts.'},
+ {id:'MER', name:'Meridian Lab', c:'#b79ce8', x:420, y:345,
+ produces:{samples:3}, consumes:{water:2, medicine:1},
+ stock:{water:8, medicine:6, parts:1, samples:12},
+ role:'Research station. Yields samples; needs water and medicine.'}
+];
+const STORAGE = 30;
+const DIFF = {
+ relaxed: {fuel:44, pen:3},
+ standard:{fuel:32, pen:4},
+ tense: {fuel:24, pen:5}
+};
+
+/* ---------- Mutable state ---------- */
+let S = null;
+let opts = {sound:false, motion:false, diff:'standard'};
+let ui = {sel:'HEL', ship:'hauler'};
+
+function coord(id){const s=S.set.find(x=>x.id===id); return {x:s.x, y:s.y};}
+
+function newGame(){
+ const d = DIFF[opts.diff];
+ S = {
+ turn:1, fuel:d.fuel, maxFuel:d.fuel, pen:d.pen,
+ set: BLUEPRINT.map(b=>({
+ ...b, produces:{...b.produces}, consumes:{...b.consumes},
+ stock:{water:0,medicine:0,parts:0,samples:0, ...b.stock},
+ trust:100, collapsed:false, fever:false
+ })),
+ pending:[], log:[], closed:{}, fuelMult:1,
+ stats:{fuelSpent:0, delivered:0, routes:0, negotiation:null, worstTurn:0, worstDef:0, collapses:[]}
+ };
+ log('Mission underway. Twelve turns until the transfer windows seal.', 'warn');
+ log('Move producers\u2019 surplus to consumers each turn. Watch supply-left counters.', '');
+}
+
+/* ---------- Sound ---------- */
+let actx=null;
+function beep(f,t,type){
+ if(!opts.sound) return;
+ try{
+ actx = actx || new (window.AudioContext||window.webkitAudioContext)();
+ const o=actx.createOscillator(), g=actx.createGain();
+ o.type=type||'sine'; o.frequency.value=f;
+ g.gain.value=0.05; o.connect(g); g.connect(actx.destination);
+ o.start(); g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime+(t||.12));
+ o.stop(actx.currentTime+(t||.12));
+ }catch(e){}
+}
+
+/* ---------- Logging ---------- */
+function log(msg, cls){
+ S.log.unshift({turn:S.turn, msg, cls:cls||''});
+ renderLog();
+}
+
+/* ---------- Fuel schedule / storm ---------- */
+function fuelMultFor(turn){
+ let m = 1;
+ if(turn===3) m += 0.4; // solar flare
+ if(turn>=11) m = 2; // storm surge
+ return m;
+}
+function stormLabel(turn){
+ if(turn>=11) return {t:'STORM SURGE — all fuel costs doubled', bad:true};
+ if(turn===3) return {t:'Solar flare — fuel costs +40% this turn', bad:true};
+ if(turn>=10) return {t:'Cinder Foundry window sealed by the storm front', bad:true};
+ if(turn>=6 && turn<=8) return {t:'Fever at Kestrel Haven — extra medicine draw', bad:true};
+ return {t:'Transfer windows stable. Fuel costs nominal.', bad:false};
+}
+
+/* ---------- Turn start events ---------- */
+function startTurn(){
+ S.fuelMult = fuelMultFor(S.turn);
+ // Cinder window seal
+ if(S.turn>=10 && !S.closed.CND){
+ S.closed.CND = true;
+ log('The storm front has sealed the transfer window to Cinder Foundry. It is now unreachable.', 'bad');
+ // drop any pending routes touching CND
+ S.pending = S.pending.filter(r=> r.from!=='CND' && r.to!=='CND');
+ }
+ // Kestrel fever
+ const kes = S.set.find(s=>s.id==='KES');
+ if(S.turn===6 && !kes.fever){ kes.fever=true; log('Outbreak at Kestrel Haven: its infirmaries now consume 3 extra medicine per turn (turns 6\u20138).', 'bad'); }
+ if(S.turn===9 && kes.fever){ kes.fever=false; log('The Kestrel Haven outbreak is contained. Medicine draw returns to normal.', 'good'); }
+ if(S.turn===3){ log('Solar flare! Ion wash raises fuel costs by 40% this turn only.', 'warn'); }
+ if(S.turn===11){ log('Storm surge — every transfer now burns double fuel. Final push.', 'bad'); }
+ // Negotiation at turn 4
+ if(S.turn===4 && S.stats.negotiation===null){ openNegotiation(); }
+ render();
+}
+
+/* effective per-turn consumption incl. fever */
+function consumesOf(s){
+ const c={...s.consumes};
+ if(s.fever) c.medicine=(c.medicine||0)+3;
+ return c;
+}
+
+/* ---------- Helpers ---------- */
+function reservedOut(id,res){
+ return S.pending.filter(r=>r.from===id && r.res===res).reduce((a,r)=>a+r.amt,0);
+}
+function availAt(id,res){
+ const s=S.set.find(x=>x.id===id);
+ return Math.max(0, s.stock[res] - reservedOut(id,res));
+}
+function routeFuel(shipK){
+ return Math.max(1, Math.round(SC[shipK].fuel * S.fuelMult));
+}
+function pendingFuel(){ return S.pending.reduce((a,r)=>a+r.fuel,0); }
+
+/* supply-left: turns until the settlement's primary need empties, given net */
+function needInfo(s){
+ const cons = consumesOf(s);
+ const keys = Object.keys(cons);
+ if(!keys.length) return null;
+ // pick the tightest need
+ let tight=null;
+ keys.forEach(k=>{
+ const left = cons[k]>0 ? Math.floor(s.stock[k]/cons[k]) : 99;
+ if(!tight || left<tight.left) tight={res:k, left, per:cons[k], stock:s.stock[k]};
+ });
+ return tight;
+}
+
+/* ---------- Rendering ---------- */
+function render(){
+ renderTop(); renderMap(); renderCards(); renderTimeline();
+ renderShips(); renderSelects(); renderEst(); renderPending();
+ renderDetail();
+}
+
+function renderTop(){
+ document.getElementById('turnNum').innerHTML = S.turn+'<span style="font-size:12px;color:var(--muted)">/12</span>';
+ document.getElementById('fuelNum').textContent = S.fuel;
+ document.getElementById('fuelFill').style.width = Math.max(0,(S.fuel/S.maxFuel*100))+'%';
+ const coop = Math.round(S.set.reduce((a,s)=>a+s.trust,0)/S.set.length);
+ const cn=document.getElementById('coopNum');
+ cn.textContent = coop+'%';
+ cn.style.color = coop>=60?'var(--green)':coop>=30?'var(--amber)':'var(--red)';
+ document.getElementById('endBtn').disabled = false;
+}
+
+function renderMap(){
+ const map=document.getElementById('map');
+ let s='';
+ // lanes (all pairs)
+ for(let i=0;i<S.set.length;i++) for(let j=i+1;j<S.set.length;j++){
+ const a=S.set[i], b=S.set[j];
+ const closed = S.closed[a.id]||S.closed[b.id];
+ s+=`<line class="lane${closed?' closed':''}" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}"/>`;
+ }
+ // pending route arcs
+ S.pending.forEach(r=>{
+ const a=coord(r.from), b=coord(r.to), col=RC[r.res].c;
+ s+=`<line class="routeline" x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="${col}"/>`;
+ });
+ s+=`<g id="ships-layer"></g>`;
+ // nodes
+ S.set.forEach(node=>{
+ const closed=S.closed[node.id];
+ const ni=needInfo(node);
+ const alert = node.collapsed ? '#e88a8a' : (ni && ni.left<=1 ? '#e88a8a' : null);
+ const sel = ui.sel===node.id;
+ s+=`<g class="node${sel?' sel':''}${closed?' closed':''}" data-id="${node.id}" tabindex="0" role="button" aria-label="${node.name}. Trust ${Math.round(node.trust)} percent">
+ <circle class="ring" cx="${node.x}" cy="${node.y}" r="26" fill="none" stroke="${sel?'#5fd0c5':'#2a3c5a'}"/>
+ <circle class="core" cx="${node.x}" cy="${node.y}" r="18" fill="${node.c}"/>
+ <text class="nlabel" x="${node.x}" y="${node.y+42}" text-anchor="middle">${node.name}</text>
+ <text class="nsub" x="${node.x}" y="${node.y+55}" text-anchor="middle">trust ${Math.round(node.trust)}%</text>`;
+ if(alert && !closed) s+=`<text class="warn" x="${node.x}" y="${node.y-24}" text-anchor="middle">\u26A0 shortage</text>`;
+ if(closed) s+=`<text class="warn" x="${node.x}" y="${node.y-24}" text-anchor="middle">sealed</text>`;
+ s+=`</g>`;
+ });
+ map.innerHTML=s;
+ map.querySelectorAll('.node').forEach(g=>{
+ const id=g.getAttribute('data-id');
+ g.addEventListener('click', ()=>{ ui.sel=id; render(); });
+ g.addEventListener('keydown', e=>{ if(e.key==='Enter'||e.key===' '){e.preventDefault(); ui.sel=id; render();}});
+ });
+}
+
+function renderCards(){
+ const wrap=document.getElementById('cards'); let s='';
+ S.set.forEach(node=>{
+ const ni=needInfo(node);
+ const alert = ni && ni.left<=1 && !node.collapsed;
+ const tcol = node.trust>=60?'var(--green)':node.trust>=30?'var(--amber)':'var(--red)';
+ s+=`<div class="scard${ui.sel===node.id?' sel':''}" data-id="${node.id}" tabindex="0" role="button">
+ <div class="nm"><span class="dot" style="background:${node.c}"></span>${node.name}</div>
+ <div class="trust"><i style="width:${node.collapsed?0:node.trust}%; background:${tcol}"></i></div>`;
+ if(node.collapsed){
+ s+=`<div class="need alert"><span>Status</span><b>COLLAPSED</b></div>`;
+ }else if(ni){
+ s+=`<div class="need${alert?' alert':''}"><span>needs ${RC[ni.res].n.toLowerCase()}</span><b>${ni.left>=99?'\u221E':ni.left+'t left'}</b></div>`;
+ }else{
+ s+=`<div class="need"><span>self-sufficient</span><b>ok</b></div>`;
+ }
+ s+=`</div>`;
+ });
+ wrap.innerHTML=s;
+ wrap.querySelectorAll('.scard').forEach(c=>{
+ const id=c.getAttribute('data-id');
+ c.onclick=()=>{ui.sel=id; render();};
+ c.onkeydown=e=>{if(e.key==='Enter'||e.key===' '){e.preventDefault(); ui.sel=id; render();}};
+ });
+}
+
+function renderTimeline(){
+ const t=document.getElementById('timeline'); let s='';
+ const evTurns={3:'#f4b860',4:'#b79ce8',6:'#e88a8a',10:'#e88a8a',11:'#e88a8a'};
+ for(let i=1;i<=12;i++){
+ const cls = i===S.turn?'now':(i<S.turn?'past':'');
+ s+=`<div class="tick ${cls}">${i}${evTurns[i]?`<span class="ev" style="background:${evTurns[i]}"></span>`:''}</div>`;
+ }
+ t.innerHTML=s;
+ const sl=stormLabel(S.turn);
+ document.getElementById('stormNote').innerHTML = sl.bad?`<b>\u26A1 ${sl.t}</b>`:sl.t;
+}
+
+function renderShips(){
+ const w=document.getElementById('ships'); let s='';
+ SHIPS.forEach(sh=>{
+ s+=`<label class="${ui.ship===sh.k?'on':''}"><input type="radio" name="ship" value="${sh.k}" ${ui.ship===sh.k?'checked':''}>
+ <b>${sh.n}</b>cap ${sh.cap} · fuel ${sh.fuel}</label>`;
+ });
+ w.innerHTML=s;
+ w.querySelectorAll('input').forEach(inp=>inp.onchange=()=>{ui.ship=inp.value; renderShips(); renderEst();});
+}
+
+function renderSelects(){
+ const from=document.getElementById('fromSel'), to=document.getElementById('toSel'), res=document.getElementById('resSel');
+ const openSet = S.set.filter(x=>!S.closed[x.id] && !x.collapsed);
+ const optHtml = arr=>arr.map(x=>`<option value="${x.id}">${x.name}</option>`).join('');
+ const pf=from.value, pt=to.value, pr=res.value;
+ from.innerHTML=optHtml(openSet);
+ to.innerHTML=optHtml(openSet);
+ res.innerHTML=RES.map(r=>`<option value="${r.k}">${r.n}</option>`).join('');
+ if(pf && openSet.find(x=>x.id===pf)) from.value=pf;
+ if(pt && openSet.find(x=>x.id===pt)) to.value=pt; else { const alt=openSet.find(x=>x.id!==from.value); if(alt) to.value=alt.id; }
+ if(pr) res.value=pr;
+ from.onchange=renderEst; to.onchange=renderEst; res.onchange=renderEst;
+ document.getElementById('amtInp').oninput=renderEst;
+}
+
+function renderEst(){
+ const from=document.getElementById('fromSel').value;
+ const res=document.getElementById('resSel').value;
+ const ship=SC[ui.ship];
+ const amt=parseInt(document.getElementById('amtInp').value)||0;
+ const avail=availAt(from,res);
+ document.getElementById('amtInp').max=Math.min(ship.cap, avail);
+ const fuel=routeFuel(ui.ship);
+ const e=document.getElementById('estLine');
+ e.innerHTML=`Available at origin: <b style="color:${RC[res].c}">${avail} ${RC[res].n.toLowerCase()}</b> · capacity ${ship.cap} · fuel cost <b>${fuel}</b>${S.fuelMult!==1?' (storm-adjusted)':''}`;
+}
+
+function renderPending(){
+ const w=document.getElementById('plist');
+ if(!S.pending.length){ w.innerHTML=`<div class="empty">No routes queued. Build a route above, then End Turn.</div>`; return; }
+ let s='';
+ S.pending.forEach((r,i)=>{
+ s+=`<div class="prow">
+ <span class="dot" style="background:${RC[r.res].c}"></span>
+ <span><b>${r.amt}</b> ${RC[r.res].n} · ${r.from}\u2192${r.to} · ${SC[r.ship].n} · \u26FD${r.fuel}</span>
+ <button class="x" data-i="${i}" aria-label="Remove route">\u00D7</button></div>`;
+ });
+ s+=`<div class="est" style="margin-top:6px">Queued fuel: <b>${pendingFuel()}</b> / ${S.fuel} available</div>`;
+ w.innerHTML=s;
+ w.querySelectorAll('.x').forEach(b=>b.onclick=()=>{ S.pending.splice(+b.getAttribute('data-i'),1); render(); beep(220,.08,'triangle');});
+}
+
+function renderDetail(){
+ const s=S.set.find(x=>x.id===ui.sel);
+ document.getElementById('detailTitle').textContent=s.name;
+ const d=document.getElementById('detail');
+ const prod=Object.entries(s.produces).map(([k,v])=>`+${v} ${RC[k].n.toLowerCase()}`).join(', ')||'\u2014';
+ const cons=Object.entries(consumesOf(s)).map(([k,v])=>`\u2212${v} ${RC[k].n.toLowerCase()}`).join(', ')||'\u2014';
+ let html=`<p style="color:var(--muted); font-size:12.5px; margin:0 0 8px">${s.role}</p>
+ <div class="drow"><span>Status</span><b style="color:${s.collapsed?'var(--red)':'var(--green)'}">${s.collapsed?'Collapsed':'Operational'}${S.closed[s.id]?' · window sealed':''}</b></div>
+ <div class="drow"><span>Trust</span><b>${Math.round(s.trust)}%</b></div>
+ <div class="drow"><span>Produces / turn</span><b>${prod}</b></div>
+ <div class="drow"><span>Consumes / turn</span><b>${cons}</b></div>
+ <div style="margin-top:10px">`;
+ RES.forEach(r=>{
+ const v=s.stock[r.k]; const pct=Math.min(100, v/STORAGE*100);
+ html+=`<div class="resline"><span class="dot" style="background:${r.c}"></span>
+ <div class="resbar"><i style="width:${pct}%; background:${r.c}"></i><b>${v}</b></div></div>`;
+ });
+ html+=`</div>`;
+ d.innerHTML=html;
+}
+
+function renderLog(){
+ const w=document.getElementById('log');
+ w.innerHTML=S.log.slice(0,40).map(l=>`<div class="lrow ${l.cls}"><span class="t">Turn ${l.turn}</span><br>${l.msg}</div>`).join('');
+}
+
+/* ---------- Actions ---------- */
+function addRoute(){
+ const from=document.getElementById('fromSel').value;
+ const to=document.getElementById('toSel').value;
+ const res=document.getElementById('resSel').value;
+ let amt=parseInt(document.getElementById('amtInp').value)||0;
+ if(from===to){ log('A route must connect two different settlements.','warn'); beep(180,.1,'square'); return; }
+ const cap=SC[ui.ship].cap, avail=availAt(from,res);
+ amt=Math.min(amt, cap, avail);
+ if(amt<=0){ log(`No ${RC[res].n.toLowerCase()} available to ship from ${from} (already reserved or empty).`,'warn'); beep(180,.1,'square'); return; }
+ const fuel=routeFuel(ui.ship);
+ if(pendingFuel()+fuel > S.fuel){ log('Not enough fuel for that route this turn.','warn'); beep(180,.1,'square'); return; }
+ S.pending.push({ship:ui.ship, from, to, res, amt, fuel});
+ beep(520,.08,'sine');
+ render();
+}
+
+async function endTurn(){
+ document.getElementById('endBtn').disabled=true;
+ // 1. Animate + execute deliveries
+ await animateRoutes();
+ let deliveredThisTurn=0;
+ S.pending.forEach(r=>{
+ const a=S.set.find(x=>x.id===r.from), b=S.set.find(x=>x.id===r.to);
+ const move=Math.min(r.amt, a.stock[r.res]);
+ a.stock[r.res]-=move;
+ const space=STORAGE-b.stock[r.res];
+ const landed=Math.min(move, space);
+ b.stock[r.res]+=landed;
+ if(landed<move) log(`${b.name} storage full: ${move-landed} ${RC[r.res].n.toLowerCase()} vented to space.`,'warn');
+ S.fuel-=r.fuel; S.stats.fuelSpent+=r.fuel; S.stats.routes++;
+ deliveredThisTurn+=landed;
+ });
+ S.stats.delivered+=deliveredThisTurn;
+ if(deliveredThisTurn) log(`Ships docked: ${deliveredThisTurn} units delivered across ${S.pending.length} routes.`,'good');
+ S.pending=[];
+
+ // 2. Consumption (uses stock incl. deliveries), trust adjust
+ let worstDef=0;
+ S.set.forEach(s=>{
+ if(s.collapsed) return;
+ const cons=consumesOf(s); let deficit=0, met=true;
+ Object.entries(cons).forEach(([k,need])=>{
+ const use=Math.min(s.stock[k], need);
+ s.stock[k]-=use;
+ const d=need-use; deficit+=d; if(d>0) met=false;
+ });
+ if(deficit>0){
+ s.trust=Math.max(0, s.trust - deficit*S.pen);
+ log(`${s.name} short by ${deficit} on its needs \u2014 trust falls to ${Math.round(s.trust)}%.`,'bad');
+ if(deficit>worstDef) worstDef=deficit;
+ }else if(Object.keys(cons).length){
+ s.trust=Math.min(100, s.trust+2);
+ }
+ if(s.trust<=0 && !s.collapsed){
+ s.collapsed=true;
+ S.stats.collapses.push({name:s.name, turn:S.turn});
+ log(`\u2620 ${s.name} has gone dark. Its life-support failed under sustained shortage.`,'bad');
+ beep(120,.4,'sawtooth');
+ }
+ });
+ if(worstDef>S.stats.worstDef){ S.stats.worstDef=worstDef; S.stats.worstTurn=S.turn; }
+
+ // 3. Production (scaled by trust)
+ S.set.forEach(s=>{
+ if(s.collapsed) return;
+ const f = s.trust>=50?1 : s.trust>=25?0.6 : 0.3;
+ Object.entries(s.produces).forEach(([k,v])=>{
+ const add=Math.round(v*f);
+ s.stock[k]=Math.min(STORAGE, s.stock[k]+add);
+ });
+ });
+
+ beep(440,.12,'sine');
+ // 4. Advance / end
+ if(S.turn>=12){ endGame(); return; }
+ S.turn++;
+ startTurn();
+ document.getElementById('endBtn').disabled=false;
+}
+
+/* animate ship dots along their routes */
+function animateRoutes(){
+ return new Promise(res=>{
+ if(opts.motion || !S.pending.length){ res(); return; }
+ const layer=document.getElementById('ships-layer');
+ const dur=800, start=performance.now();
+ const dots=S.pending.map(r=>{
+ const a=coord(r.from), b=coord(r.to);
+ const c=document.createElementNS('http://www.w3.org/2000/svg','circle');
+ c.setAttribute('r','5'); c.setAttribute('fill',RC[r.res].c);
+ c.setAttribute('stroke','#0c1522'); c.setAttribute('stroke-width','1.5');
+ layer.appendChild(c);
+ return {c,a,b};
+ });
+ function frame(now){
+ const p=Math.min(1,(now-start)/dur);
+ const e=p<.5?2*p*p:1-Math.pow(-2*p+2,2)/2;
+ dots.forEach(d=>{
+ d.c.setAttribute('cx', d.a.x+(d.b.x-d.a.x)*e);
+ d.c.setAttribute('cy', d.a.y+(d.b.y-d.a.y)*e);
+ });
+ if(p<1) requestAnimationFrame(frame);
+ else { dots.forEach(d=>d.c.remove()); res(); }
+ }
+ requestAnimationFrame(frame);
+ });
+}
+
+/* ---------- Negotiation ---------- */
+function openNegotiation(){
+ const ov=document.getElementById('negOverlay');
+ document.getElementById('negTitle').textContent='Verdant Ring proposes an expansion pact';
+ document.getElementById('negBody').textContent='Verdant Ring will immediately ship 12 water to whoever needs it most and boost water output by +2/turn \u2014 but its growing crews will draw +1 extra medicine every turn for the rest of the mission. The council awaits your call.';
+ const ch=document.getElementById('negChoices');
+ ch.innerHTML='';
+ const accept=document.createElement('button');
+ accept.className='choice';
+ accept.innerHTML='<b>Accept the pact</b><small>+12 water now to Helios & Meridian, +2 water/turn \u2014 but Verdant needs +1 medicine/turn forever.</small>';
+ accept.onclick=()=>{
+ const vrd=S.set.find(s=>s.id==='VRD');
+ vrd.produces.water+=2; vrd.consumes.medicine+=1;
+ const hel=S.set.find(s=>s.id==='HEL'), mer=S.set.find(s=>s.id==='MER');
+ hel.stock.water=Math.min(STORAGE,hel.stock.water+7);
+ mer.stock.water=Math.min(STORAGE,mer.stock.water+5);
+ S.stats.negotiation='accepted';
+ log('Pact accepted. Water flows freely \u2014 but Verdant Ring now demands more medicine each turn. Keep Kestrel\u2019s supply lines open.','warn');
+ ov.classList.remove('active'); beep(600,.15); render();
+ };
+ const decline=document.createElement('button');
+ decline.className='choice';
+ decline.innerHTML='<b>Decline politely</b><small>No change to the economy. Verdant respects your caution: +6 trust.</small>';
+ decline.onclick=()=>{
+ const vrd=S.set.find(s=>s.id==='VRD'); vrd.trust=Math.min(100,vrd.trust+6);
+ S.stats.negotiation='declined';
+ log('Pact declined. Verdant Ring appreciates the steady hand \u2014 its trust rises.','good');
+ ov.classList.remove('active'); beep(400,.15); render();
+ };
+ ch.appendChild(accept); ch.appendChild(decline);
+ ov.classList.add('active');
+}
+
+/* ---------- End game ---------- */
+function endGame(){
+ const survivors=S.set.filter(s=>!s.collapsed);
+ const won = S.set.every(s=>!s.collapsed);
+ const avg=Math.round(S.set.reduce((a,s)=>a+s.trust,0)/S.set.length);
+ const sumScreen=document.getElementById('summary');
+ const head=document.getElementById('sumhead');
+ head.parentElement.classList.remove('win','lose');
+ document.querySelector('#summary .panel').classList.remove('win','lose');
+ document.querySelector('#summary .panel').classList.add(won?'win':'lose');
+
+ const v=document.getElementById('verdict'), vs=document.getElementById('verdictSub');
+ if(won){
+ let grade = avg>=75?'Flourishing Convoy':avg>=50?'Convoy Intact':'Battered but Breathing';
+ v.textContent='All Five Survived';
+ vs.textContent=`The storm has sealed the windows and every settlement is still lit. Cooperative health: ${avg}%. Rating: ${grade}.`;
+ }else{
+ v.textContent = survivors.length? `${5-survivors.length} Settlement${5-survivors.length>1?'s':''} Lost` : 'Total Collapse';
+ vs.textContent='When the windows sealed, some settlements had already gone dark. The cooperative did not hold together.';
+ }
+
+ // final list
+ const fl=document.getElementById('finalList'); let f='';
+ S.set.forEach(s=>{
+ const col=s.collapsed?'var(--red)':s.trust>=60?'var(--green)':'var(--amber)';
+ f+=`<div class="sfinal"><span><span class="dot" style="display:inline-block;background:${s.c}"></span> ${s.name}</span>
+ <b style="color:${col}">${s.collapsed?'collapsed':Math.round(s.trust)+'% trust'}</b></div>`;
+ });
+ fl.innerHTML=f;
+
+ // decisive choices narrative
+ const dec=document.getElementById('decisive'); let d='';
+ d+=`<li>You committed <b>${S.stats.routes} routes</b> and delivered <b>${S.stats.delivered} units</b>, burning <b>${S.stats.fuelSpent} fuel</b>.</li>`;
+ if(S.stats.negotiation==='accepted') d+=`<li>Accepting the <b>Verdant expansion pact</b> loosened water supply but strained the medicine chain — the deciding gamble of the run.</li>`;
+ else if(S.stats.negotiation==='declined') d+=`<li>Declining the <b>Verdant pact</b> kept the economy predictable; you traded a water windfall for stability.</li>`;
+ if(S.stats.worstDef>0) d+=`<li>Your tightest moment came on <b>turn ${S.stats.worstTurn}</b>, when a shortfall of <b>${S.stats.worstDef} units</b> cut deepest into trust.</li>`;
+ if(S.stats.collapses.length){
+ S.stats.collapses.forEach(c=> d+=`<li><b>${c.name}</b> collapsed on turn ${c.turn} — the cascade likely began one or two turns earlier when its supplier fell behind.</li>`);
+ } else {
+ d+=`<li>No settlement ever fell below the line — the parts→medicine→water→samples loop stayed unbroken all twelve turns.</li>`;
+ }
+ if(won && avg<50) d+=`<li>Survival was narrow: several settlements ran on fumes. More fuel spent on the water and medicine loops early would have widened the margin.</li>`;
+ dec.innerHTML=d;
+
+ show('summary');
+ beep(won?660:160,.4, won?'sine':'sawtooth');
+}
+
+/* ---------- Tutorial ---------- */
+const TUT=[
+ {t:'Welcome, Convoy Commander', b:'Five settlements survive by trading four goods: water, medicine, reactor parts, and research samples. Each PRODUCES one good and NEEDS another. Your job is to move surplus from producers to consumers before the solar storm seals the windows in 12 turns.'},
+ {t:'Read the Star Map', b:'Each glowing node is a settlement; faint lines are transfer lanes. A \u26A0 shortage badge means that settlement is about to run out of what it needs. Click any node — on the map or in the cards — to open its detail panel on the right.'},
+ {t:'Anticipate the cascade', b:'The settlement cards show "turns of supply left". When a producer starves, its trust drops and its output falls — which starves ITS customers next. Verdant water feeds Helios & Meridian; Kestrel medicine feeds Verdant & Meridian. Watch the counters and you can see a shortage two turns before it bites.'},
+ {t:'Plan your cargo', b:'In the Cargo Planner pick a ship (Courier is cheap & small, Freighter is thirsty & huge), an origin, a destination, a good, and an amount. Add as many routes as your fuel allows. Nothing is spent until you End Turn — remove and re-queue routes freely to revise your plan.'},
+ {t:'The storm tightens', b:'The timeline marks events: a solar flare (turn 3), a Verdant negotiation (turn 4), a Kestrel outbreak (turns 6\u20138), the Cinder window sealing (turn 10), and a fuel-doubling surge (turn 11). Fuel is your whole-mission budget — spend it where the cascade would hurt most.'},
+ {t:'Win together', b:'End turn to send your ships, run production and consumption, and advance the storm. If every settlement is still lit at turn 12, the cooperative survives — and the debrief will explain exactly which choices decided it. Good luck.'}
+];
+let tutI=0;
+function openTut(){ tutI=0; renderTut(); document.getElementById('tutOverlay').classList.add('active'); }
+function renderTut(){
+ document.getElementById('tutTitle').textContent=TUT[tutI].t;
+ document.getElementById('tutBody').textContent=TUT[tutI].b;
+ document.getElementById('tutDots').innerHTML=TUT.map((_,i)=>`<i class="${i===tutI?'on':''}"></i>`).join('');
+ document.getElementById('tutBack').disabled=tutI===0;
+ document.getElementById('tutNext').textContent=tutI===TUT.length-1?'Got it':'Next';
+}
+
+/* ---------- Screen control ---------- */
+function show(id){
+ document.querySelectorAll('.screen').forEach(s=>s.classList.remove('active'));
+ document.getElementById(id).classList.add('active');
+}
+function applyMotion(){ document.body.classList.toggle('reduce', opts.motion); }
+
+/* ---------- Wiring ---------- */
+document.querySelectorAll('.diff').forEach(b=>{
+ b.onclick=()=>{
+ document.querySelectorAll('.diff').forEach(x=>x.setAttribute('aria-pressed','false'));
+ b.setAttribute('aria-pressed','true'); opts.diff=b.getAttribute('data-diff');
+ };
+});
+document.getElementById('optSound').onchange=e=>{opts.sound=e.target.checked;};
+document.getElementById('optMotion').onchange=e=>{opts.motion=e.target.checked; applyMotion();};
+document.getElementById('optSound2').onchange=e=>{opts.sound=e.target.checked; document.getElementById('optSound').checked=e.target.checked;};
+document.getElementById('optMotion2').onchange=e=>{opts.motion=e.target.checked; document.getElementById('optMotion').checked=e.target.checked; applyMotion();};
+
+document.getElementById('startBtn').onclick=()=>{ newGame(); show('game'); render(); startTurn(); beep(520,.15);};
+document.getElementById('tutBtn').onclick=openTut;
+document.getElementById('tutBtn2').onclick=openTut;
+document.getElementById('tutBtn3').onclick=()=>{document.getElementById('pauseOverlay').classList.remove('active'); openTut();};
+document.getElementById('tutNext').onclick=()=>{ if(tutI<TUT.length-1){tutI++; renderTut();} else document.getElementById('tutOverlay').classList.remove('active'); };
+document.getElementById('tutBack').onclick=()=>{ if(tutI>0){tutI--; renderTut();} };
+
+document.getElementById('addBtn').onclick=addRoute;
+document.getElementById('endBtn').onclick=endTurn;
+document.getElementById('pauseBtn').onclick=()=>document.getElementById('pauseOverlay').classList.add('active');
+document.getElementById('resumeBtn').onclick=()=>document.getElementById('pauseOverlay').classList.remove('active');
+document.getElementById('restartBtn').onclick=()=>{document.getElementById('pauseOverlay').classList.remove('active'); show('setup');};
+document.getElementById('againBtn').onclick=()=>show('setup');
+
+document.addEventListener('keydown',e=>{
+ if(document.getElementById('game').classList.contains('active') &&
+ !document.querySelector('.overlay.active') &&
+ !/^(INPUT|SELECT|TEXTAREA)$/.test(document.activeElement.tagName)){
+ if(e.key==='e'||e.key==='E'){ e.preventDefault(); if(!document.getElementById('endBtn').disabled) endTurn(); }
+ if(e.key==='?'){ e.preventDefault(); document.getElementById('pauseOverlay').classList.add('active'); }
+ }
+ if(e.key==='Escape'){ document.querySelectorAll('.overlay.active').forEach(o=>o.classList.remove('active')); }
+});
+
+// respect OS reduced-motion as a starting default
+if(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches){
+ opts.motion=true; document.getElementById('optMotion').checked=true; applyMotion();
+}
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/89e23514bd71/claude-code.png b/data/artifacts/89e23514bd71/claude-code.png
new file mode 100644
index 0000000..5ba2d0b
Binary files /dev/null and b/data/artifacts/89e23514bd71/claude-code.png differ
diff --git a/data/challenges.json b/data/challenges.json
index 8c55114..98cd9b9 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -33726,7 +33726,7 @@
"bytes": 12760,
"thumb": true,
"aiScore": 7.3,
- "aiReason": "The workspace is functional but lacks some of the requested features such as filters and a side panel for trend analysis.",
+ "aiReason": "The workspace provides necessary metrics but lacks some interactive elements and visual polish.",
"aiScores": {
"qwen2.5vl:7b": 7,
"minicpm-v:latest": 7.5
@@ -33745,7 +33745,7 @@
"bytes": 4810,
"thumb": true,
"aiScore": 7.3,
- "aiReason": "The workspace provides the required information but lacks some interactive elements and visual polish.",
+ "aiReason": "The workspace provides the required data but lacks some features like a waterfall chart and side panel for trend analysis.",
"aiScores": {
"qwen2.5vl:7b": 7,
"minicpm-v:latest": 7.5
@@ -33762,12 +33762,19 @@
"finished_at": "2026-08-31T22:52:01.148Z",
"queued_at": "2026-08-31T22:39:21.538Z",
"bytes": 48718,
- "thumb": true
+ "thumb": true,
+ "aiScore": 7.3,
+ "aiReason": "The workspace provides essential margin intelligence but lacks the detailed product family metrics and loading/no-results states required.",
+ "aiScores": {
+ "qwen2.5vl:7b": 7,
+ "minicpm-v:latest": 7.5
+ },
+ "aiSpread": 0.5
}
],
"judging": false,
"aiPick": "qwen3-14b",
- "judged_at": "2026-08-31T21:14:58.066Z"
+ "judged_at": "2026-09-01T19:34:32.103Z"
},
{
"id": "96c1603e5f4a",
@@ -33860,13 +33867,13 @@
"queued_at": "2026-08-31T21:05:53.092Z",
"bytes": 13527,
"thumb": true,
- "aiScore": 5.8,
- "aiReason": "The configurator is functional but lacks the nuanced colorways and finish levels specified in the challenge.",
+ "aiScore": 6.8,
+ "aiReason": "The configurator provides basic functionality but lacks the nuanced colorways and finish levels specified in the challenge.",
"aiScores": {
- "qwen2.5vl:7b": 4,
+ "qwen2.5vl:7b": 6,
"minicpm-v:latest": 7.5
},
- "aiSpread": 3.5
+ "aiSpread": 1.5
},
{
"model": "qwen25-7b",
@@ -33880,7 +33887,7 @@
"bytes": 7853,
"thumb": true,
"aiScore": 6.8,
- "aiReason": "The configurator is functional but lacks the nuanced colorways and finish levels specified. The visual design is simple and lacks the material library feel.",
+ "aiReason": "The configurator provides basic functionality but lacks the nuanced colorways, ground options, finish levels, lead-time ranges, memo pricing, and minimum-order rules required for a comprehensive sampl",
"aiScores": {
"qwen2.5vl:7b": 6,
"minicpm-v:latest": 7.5
@@ -33897,12 +33904,19 @@
"finished_at": "2026-08-31T22:43:50.148Z",
"queued_at": "2026-08-31T22:39:21.518Z",
"bytes": 49680,
- "thumb": true
+ "thumb": true,
+ "aiScore": 8.3,
+ "aiReason": "The configurator provides basic functionality but lacks depth in terms of colorway comparison and finish-level selection. The visual quality is adequate for a material library.",
+ "aiScores": {
+ "qwen2.5vl:7b": 7,
+ "minicpm-v:latest": 9.5
+ },
+ "aiSpread": 2.5
}
],
"judging": false,
- "aiPick": "qwen25-7b",
- "judged_at": "2026-08-31T21:20:29.513Z"
+ "aiPick": "claude-code",
+ "judged_at": "2026-09-01T19:34:47.012Z"
},
{
"id": "6e2d6635424a",
@@ -33925,7 +33939,7 @@
"bytes": 15389,
"thumb": true,
"aiScore": 7.3,
- "aiReason": "The console effectively communicates the transit control scenario but lacks some visual hierarchy and color contrast for optimal readability.",
+ "aiReason": "The console effectively fulfills the challenge by providing all necessary functions and a clear visual hierarchy. However, it could benefit from more color contrast for better readability.",
"aiScores": {
"qwen2.5vl:7b": 7,
"minicpm-v:latest": 7.5
@@ -33943,13 +33957,13 @@
"queued_at": "2026-08-31T21:05:53.114Z",
"bytes": 5303,
"thumb": true,
- "aiScore": 4.8,
- "aiReason": "The console is partially functional but lacks key elements such as incident details, network map, and decision log.",
+ "aiScore": 3.3,
+ "aiReason": "The image is mostly blank and does not provide any of the required functionality or visual elements to fulfill the challenge.",
"aiScores": {
- "qwen2.5vl:7b": 4,
- "minicpm-v:latest": 5.5
+ "qwen2.5vl:7b": 3,
+ "minicpm-v:latest": 3.5
},
- "aiSpread": 1.5
+ "aiSpread": 0.5
},
{
"model": "claude-code",
@@ -33961,12 +33975,19 @@
"finished_at": "2026-08-31T22:47:54.831Z",
"queued_at": "2026-08-31T22:39:21.533Z",
"bytes": 43945,
- "thumb": true
+ "thumb": true,
+ "aiScore": 7.3,
+ "aiReason": "The model effectively represents the challenge but lacks some visual polish and detail in the command view.",
+ "aiScores": {
+ "qwen2.5vl:7b": 7,
+ "minicpm-v:latest": 7.5
+ },
+ "aiSpread": 0.5
}
],
"judging": false,
"aiPick": "qwen3-14b",
- "judged_at": "2026-08-31T21:25:25.266Z"
+ "judged_at": "2026-09-01T19:35:04.024Z"
},
{
"id": "89e23514bd71",
@@ -33988,10 +34009,10 @@
"queued_at": "2026-08-31T21:05:53.127Z",
"bytes": 16044,
"thumb": true,
- "aiScore": 6.3,
- "aiReason": "The interface is functional but lacks the star map and detailed cargo planner as described in the challenge.",
+ "aiScore": 6.8,
+ "aiReason": "The interface is functional but lacks the detailed visual elements and interactivity required for a polished strategy game.",
"aiScores": {
- "qwen2.5vl:7b": 6,
+ "qwen2.5vl:7b": 7,
"minicpm-v:latest": 6.5
},
"aiSpread": 0.5
@@ -34007,28 +34028,37 @@
"queued_at": "2026-08-31T21:05:53.132Z",
"bytes": 8170,
"thumb": true,
- "aiScore": 4.3,
- "aiReason": "The interface is partially functional but lacks the star map and detailed settlement views required for a polished strategy game.",
+ "aiScore": 3.8,
+ "aiReason": "The interface is minimal and lacks the detailed star map, turn timeline, cargo planner, settlement detail view, event log, and interactive elements required to fulfill the challenge.",
"aiScores": {
- "qwen2.5vl:7b": 5,
+ "qwen2.5vl:7b": 4,
"minicpm-v:latest": 3.5
},
- "aiSpread": 1.5
+ "aiSpread": 0.5
},
{
"model": "claude-code",
- "status": "running",
+ "status": "done",
"error": null,
- "seconds": 301,
- "cost": null,
+ "seconds": 339,
+ "cost": 0,
"started_at": "2026-09-01T19:19:42.910Z",
- "finished_at": null,
- "queued_at": "2026-09-01T19:19:42.904Z"
+ "finished_at": "2026-09-01T19:25:22.076Z",
+ "queued_at": "2026-09-01T19:19:42.904Z",
+ "bytes": 46191,
+ "thumb": true,
+ "aiScore": 7.3,
+ "aiReason": "The interface is clear and functional but lacks the detailed star map, turn timeline, or interactive elements required for a polished strategy game.",
+ "aiScores": {
+ "qwen2.5vl:7b": 7,
+ "minicpm-v:latest": 7.5
+ },
+ "aiSpread": 0.5
}
],
"judging": false,
- "aiPick": "qwen3-14b",
- "judged_at": "2026-08-31T21:30:16.517Z"
+ "aiPick": "claude-code",
+ "judged_at": "2026-09-01T19:30:22.289Z"
},
{
"id": "c60235b46976",
diff --git a/verification/e2e-proof.json b/verification/e2e-proof.json
index 7e7be74..d847295 100644
--- a/verification/e2e-proof.json
+++ b/verification/e2e-proof.json
@@ -3,7 +3,7 @@
"risk_tier": "R4",
"environment": "Local isolation first, then Steve-approved production deployment through the Mac PM2 model-arena process and Cloudflare at https://modelarena.agentabrams.com/.",
"baseline_commit": "7e06a40",
- "timestamp": "2026-08-31T17:42:00Z",
+ "timestamp": "2026-09-01T19:31:00Z",
"checks": [
{
"boundary": "static and syntax",
@@ -41,6 +41,14 @@
"tool": "3x",
"reason": "The shared runner completed HTTP M1 but stalled during headless render on this host; the critical interaction path was independently completed in system Chrome.",
"verdict": "SKIP"
+ },
+ {
+ "boundary": "five detailed production challenges",
+ "command": "ARENA_URL=https://modelarena.agentabrams.com/ node verification/five-detailed.e2e.js",
+ "build_commit": "303f7c4",
+ "assertions": ["five named challenges visible in the live UI", "prompt lengths 1033-1139 characters", "exactly three completed artifacts per challenge", "all 15 thumbnails rendered", "all 15 artifacts panel-scored", "zero browser errors", "zero metered spend"],
+ "artifact": "verification/five-detailed-live.png",
+ "verdict": "PASS"
}
],
"negative_checks": [
diff --git a/verification/five-detailed-live.png b/verification/five-detailed-live.png
index a749aa1..4ab80bf 100644
Binary files a/verification/five-detailed-live.png and b/verification/five-detailed-live.png differ
diff --git a/verification/five-detailed-proof.json b/verification/five-detailed-proof.json
index 9347779..10c453e 100644
--- a/verification/five-detailed-proof.json
+++ b/verification/five-detailed-proof.json
@@ -19,12 +19,13 @@
"evidence": "verification/five-detailed-live.png"
},
"runOutcome": {
- "completedArtifacts": 14,
+ "completedArtifacts": 15,
"expectedArtifacts": 15,
- "verdict": "PASS with one model-level limitation",
- "detail": "Qwen 14B and Qwen 2.5 completed all five. Claude completed four after targeted retries; Orbital Freight Cooperative timed out at the Claude CLI 300-second boundary twice. The challenge remains fully comparable with two rendered, AI-scored artifacts."
+ "scoredArtifacts": 15,
+ "verdict": "PASS",
+ "detail": "All three models completed, rendered, and received panel scores for all five challenges. Orbital Freight Cooperative exposed the former fixed 300-second Claude CLI ceiling; the configurable default was raised to 600 seconds and its targeted retry completed in 339 seconds at zero marginal cost."
},
"negativeChecks": ["creation runner rejects prompts outside 700-1300 characters", "live verifier rejects missing titles, prompts below 1000 characters, fewer than three requested runs, fewer than two completed artifacts, or unexpected run failures"],
- "cleanup": "No duplicate challenges were created. Retries targeted only failed zero-cost Claude runs. Production challenge records and artifacts were intentionally retained.",
+ "cleanup": "No duplicate challenges were created. Retries targeted only the failed zero-cost Claude run. Production challenge records and all 15 artifacts were intentionally retained.",
"verdict": "PASS"
}
diff --git a/verification/five-detailed.e2e.js b/verification/five-detailed.e2e.js
index a25848e..a2dc92a 100644
--- a/verification/five-detailed.e2e.js
+++ b/verification/five-detailed.e2e.js
@@ -24,9 +24,10 @@ const titles = ['Margin Intelligence Command Center','Afterlight Exhibition Stor
for (const record of records) {
if (record.prompt.length < 1000) throw new Error(`${record.title} prompt too short: ${record.prompt.length}`);
if (record.runs.length !== 3) throw new Error(`${record.title} expected 3 runs, got ${record.runs.length}`);
- if (record.runs.filter(run => run.status === 'done').length < 2) throw new Error(`${record.title} has fewer than two completed comparison artifacts`);
- const unexpected = record.runs.filter(run => run.status === 'error' && (run.model !== 'claude-code' || !/claude CLI/.test(run.error || '')));
- if (unexpected.length) throw new Error(`${record.title} has unexpected run failure`);
+ const incomplete = record.runs.filter(run => run.status !== 'done');
+ if (incomplete.length) throw new Error(`${record.title} has incomplete runs: ${incomplete.map(run => `${run.model}:${run.status}`).join(', ')}`);
+ const unrendered = record.runs.filter(run => !run.thumb || typeof run.aiScore !== 'number');
+ if (unrendered.length) throw new Error(`${record.title} has unrendered or unscored runs: ${unrendered.map(run => run.model).join(', ')}`);
}
await page.screenshot({ path: 'verification/five-detailed-live.png' });
if (errors.length) throw new Error(`browser errors: ${errors.join(' | ')}`);
← 87400fd auto-data-snapshot: 2026-09-01T12:16:45 (1 data files) — dat
·
back to Model Arena
·
add five Grateful Dead gallery briefs dd024dc →