(() => { "use strict"; const $ = id => document.getElementById(id); const GAMES = { mario:{title:"Mario",seed:0,caption:"The model controls movement and jumping, one decision at a time.",note:"Original World 1-1 · Movement and jump choices run sequentially.",example:"/mario-example.mp4",exampleTitle:"Mario · Recorded example · 5×"}, cube:{title:"Rubik’s Cube",seed:20260930,caption:"A full 25-turn scramble. Each model choice selects a sequence of cube turns.",note:"25-turn scramble · Watch each selected algorithm unfold in 3D.",example:"/cube-example.mp4",exampleTitle:"Rubik’s Cube · Recorded example · 10×"}, tetris:{title:"Tetris",seed:0,caption:"Clear 20 lines as the pieces fall faster. Play continues at maximum speed.",note:"Natural gravity · No drop shortcut · 20-line goal.",example:"/tetris-example.mp4",exampleTitle:"Tetris · Recorded example · 5×"} }; const TERMINAL = new Set(["completed","failed","cancelled"]); const sleep = ms => new Promise(r=>setTimeout(r,ms)); const safelyRead = key=>{try{return JSON.parse(localStorage.getItem(key))}catch{return null}}; const safelyWrite = (key,value)=>{try{if(value===null)localStorage.removeItem(key);else localStorage.setItem(key,JSON.stringify(value))}catch{}}; let clientId=safelyRead("jev-arena-client"); if(typeof clientId!=="string" || clientId.length>100){clientId=crypto.randomUUID?.() || "client-"+Date.now().toString(36)+"-"+Math.random().toString(36).slice(2);safelyWrite("jev-arena-client",clientId);} let selected=Object.hasOwn(GAMES,location.hash.slice(1))?location.hash.slice(1):"mario"; let online=false, initial={}, health={}; const states=Object.fromEntries(Object.keys(GAMES).map(game=>[game,{ game,seed:GAMES[game].seed,job:null,status:"ready",view:null,decision:null,metrics:{}, queue:[],draining:false,polling:false,nextPoll:0,after:0,summary:null,message:"",error:false, restoring:false,hasMore:false,generation:0,cancelling:false,starting:false,skip:false,initialReceived:false }])); const rendererOptions={compact:window.innerWidth<=500}; const renderer=ArenaRenderer.create($("game-canvas"),rendererOptions); const reducedMotion=matchMedia("(prefers-reduced-motion: reduce)").matches; const format=value=>Number.isFinite(Number(value))?Number(value).toLocaleString("en-US"):"—"; const pretty=value=>String(value??"—").replaceAll("_"," "); function persist(s){safelyWrite("jev-arena-"+s.game,s.job?{...s.job,game:s.game,seed:s.seed}:null);} function isRunning(s){return !!s.job&&!TERMINAL.has(s.status);} function isBusy(s){return s.starting||isRunning(s)||s.draining||s.hasMore||s.queue.length>0;} function successOf(s){return s.summary?.success??s.summary?.result?.success??s.view?.success??s.view?.hud?.success??false;} function stateLabel(s){ if(s.starting)return "Starting"; if(s.cancelling)return "Stopping"; if(s.status==="queued")return "Queued"; if(s.status==="running")return "Running"; if(s.status==="completed"&&(s.draining||s.hasMore||s.queue.length))return "Playing moves"; if(s.status==="completed")return successOf(s)?"Completed":"Run finished"; if(s.status==="failed")return "Run failed"; if(s.status==="cancelled")return "Stopped"; return "Ready"; } function notice(s){ let text=s.message; if(!text&&s.status==="queued")text="Your run is queued. It will start when this game’s current run finishes."; if(!text&&s.status==="completed"&&!isBusy(s)&&!successOf(s))text="This run ended before the goal. Start again or try another seed."; if(!text&&s.status==="failed")text="The run could not finish. Start again to try a fresh run."; $("notice").hidden=!text;$("notice").textContent=text;$("notice").classList.toggle("error",s.error||s.status==="failed"); } function metric(id,value){ const el=$(id);el.replaceChildren(); el.append(document.createTextNode(Number.isFinite(Number(value))&&value!==null&&value!==undefined?Number(value).toFixed(1):"—")); const unit=document.createElement("small");unit.textContent="ms";el.append(unit); } function renderDecisions(s){ metric("current-inference",s.metrics.current_inference_ms);metric("average-inference",s.metrics.average_inference_ms); const count=s.metrics.calls??0;$("call-count").textContent=format(count)+(count===1?" call":" calls"); $("chosen-action").textContent=s.decision?.action??(isRunning(s)?"Awaiting decision":"Waiting to start"); $("decision-description").textContent=s.decision?.description??(isRunning(s)?"The model is evaluating the current game state.":"Choose a game and start the model."); const options=$("options");options.replaceChildren(); const entries=Array.isArray(s.decision?.options)?s.decision.options:[]; const probs=Array.isArray(s.decision?.probabilities)?s.decision.probabilities:[]; const subdecisions=Array.isArray(s.decision?.subdecisions)?s.decision.subdecisions:[]; const selectedChoice=s.decision?.choice??subdecisions.at(-1)?.choice??s.decision?.action; entries.forEach((option,i)=>{ const label=typeof option==="object"?(option.label??option.name??option.action??"Option "+(i+1)):String(option); const probability=Number(probs[i]); const row=document.createElement("div");row.className="option-row"; const valid=Number.isFinite(probability)&&probability>=0; row.style.setProperty("--probability",(valid?Math.min(1,probability)*100:0)+"%"); row.classList.toggle("chosen",label===selectedChoice); const name=document.createElement("span");name.className="option-name";name.textContent=label;name.title=label; const value=document.createElement("span");value.className="option-value";value.textContent=valid?(probability*100).toFixed(1)+"%":"—"; row.append(name,value);options.append(row); }); $("subdecisions").replaceChildren(...subdecisions.map(sub=>{ const row=document.createElement("div");row.className="subdecision"; const name=document.createElement("span");name.className="subdecision-name";name.textContent=pretty(sub.name); const choice=document.createElement("span");choice.className="subdecision-choice";choice.textContent=pretty(sub.choice); const time=document.createElement("span");time.className="subdecision-time";time.textContent=Number.isFinite(sub.inference_ms)?sub.inference_ms.toFixed(1)+" ms":"—"; row.append(name,choice,time);return row; })); } function renderStats(s){ const v=s.view??initial[s.game], h=v?.hud??{}; let rows=[],progress=""; if(s.game==="mario"){ progress="World "+(h.world??1)+"-"+(h.stage??1); rows=[["Position",format(h.x_position)+" px"],["Score",format(h.score??0)],["Coins",format(h.coins??0)],["Decisions",format(h.steps??0)],["Action",pretty(v?.last_action)],["Goal",h.success?"Flag reached":"Reach the flag"]]; }else if(s.game==="cube"){ progress=(v?.solved_faces??0)+" / 6 faces"; rows=[["Faces",format(v?.solved_faces??0)+" / 6"],["Stickers",format(v?.matched_stickers??0)+" / 54"],["Quarter turns",format(v?.primitive_steps??0)],["Decisions",format(v?.steps??0)],["Phase",pretty(v?.phase)],["Scramble","25 turns"]]; }else{ progress=(v?.lines??0)+" / 20 lines"; rows=[["Lines",format(v?.lines??0)+" / 20"],["Score",format(v?.score??0)],["Pieces",format(v?.pieces??0)],["Gravity",v?.gravity_ms?Math.round(v.gravity_ms)+" ms / row":"100 ms / row"],["Speed",v?.maximum_speed_reached?"Maximum":((v?.speed_multiplier??1).toFixed(2)+"×")],["Decisions",format(v?.steps??0)]]; } $("game-progress").textContent=progress; $("stats").replaceChildren(...rows.map(([label,value])=>{ const div=document.createElement("div");div.className="stat"; const dt=document.createElement("dt");dt.textContent=label; const dd=document.createElement("dd");dd.textContent=value;div.append(dt,dd);return div; })); } function controls(s){ $("start").disabled=isBusy(s)||!online; $("start").lastChild.textContent=s.job&&TERMINAL.has(s.status)?" Run again":" Start model"; $("stop").hidden=!isBusy(s); $("stop").disabled=s.cancelling||s.starting; $("stop").textContent=TERMINAL.has(s.status)&&(s.draining||s.queue.length)?"Skip animation":"Stop run"; $("seed").disabled=isBusy(s)||s.game==="mario"; $("shuffle").hidden=s.game==="mario";$("shuffle").disabled=isBusy(s); const pill=$("state-pill");pill.textContent=stateLabel(s); pill.className="state-pill"+(isBusy(s)?" running":(s.status==="completed"&&successOf(s)?" success":s.status==="failed"?" failed":"")); $("stage-label").textContent=isBusy(s)?(s.status==="queued"?"Waiting for a slot":s.status==="completed"?"Selected moves · playback":"Live run"):s.status==="ready"?"Opening state":s.status==="cancelled"?"Run stopped":"Run result"; $("canvas-loading").hidden=!(s.starting||(isRunning(s)&&!s.initialReceived&&s.status!=="queued")); notice(s); for(const [game,state] of Object.entries(states))$("tab-status-"+game).classList.toggle("active",isBusy(state)); } function safeRender(view,context=null){ try{const result=renderer.render(view,context);if(result?.catch)result.catch(()=>{});}catch{} } function paint(s,draw=true){ if(selected!==s.game)return; controls(s);renderDecisions(s);renderStats(s); if(draw)safeRender(s.view??initial[s.game]??null); } function selectGame(game,updateHash=true){ if(!Object.hasOwn(GAMES,game))return; selected=game; for(const tab of document.querySelectorAll(".game-tab")){const active=tab.dataset.game===game;tab.setAttribute("aria-selected",String(active));tab.tabIndex=active?0:-1;} $("game-panel").setAttribute("aria-labelledby","tab-"+game); $("game-title").textContent=GAMES[game].title; $("game-caption").textContent=GAMES[game].caption; $("game-note").textContent=GAMES[game].note; $("seed").value=states[game].seed; $("game-canvas").setAttribute("aria-label",GAMES[game].title+" game view"); if(updateHash)history.replaceState(null,"","#"+game); paint(states[game]); } async function request(url,options={}){ const controller=new AbortController();const timeout=setTimeout(()=>controller.abort(),18000); try{ const response=await fetch(url,{...options,signal:controller.signal,cache:"no-store"}); let body;try{body=await response.json()}catch{body={}}; if(!response.ok){ const detail=typeof body.detail==="string"?body.detail:typeof body.error==="string"?body.error:"Request failed ("+response.status+")"; const error=new Error(detail);error.status=response.status;throw error; } return body; }catch(error){if(error.name==="AbortError")throw new Error("Connection timed out. Please try again.");throw error;} finally{clearTimeout(timeout);} } async function start(){ const s=states[selected];if(isBusy(s))return; const seed=Number($("seed").value); if(!Number.isInteger(seed)||seed<0||seed>2147483647){s.message="Enter a whole-number seed between 0 and 2147483647.";s.error=true;paint(s,false);return;} s.starting=true;s.message="";s.error=false;paint(s,false); try{ const job=await request("/api/arena/jobs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({game:s.game,seed,client_id:clientId})}); if(!job.id||!job.ticket)throw new Error("The server did not return a run ticket. Please try again."); Object.assign(s,{seed,job,status:job.status??"queued",after:0,summary:null,view:null,decision:null,metrics:{},queue:[],nextPoll:0,restoring:false,hasMore:false,initialReceived:false,skip:false}); s.generation++;persist(s); }catch(error){s.message=error.message;s.error=true;} finally{s.starting=false;paint(s);} } async function stop(){ const s=states[selected]; if(TERMINAL.has(s.status)){s.skip=true;return;} if(!s.job||s.cancelling)return; s.cancelling=true;s.message="";paint(s,false); try{ const result=await request("/api/arena/jobs/"+encodeURIComponent(s.job.id)+"/cancel",{method:"POST",headers:{"X-Job-Ticket":s.job.ticket}}); if(result.status==="cancelled"){s.status="cancelled";s.skip=true;} s.message="Stopping this run…"; s.nextPoll=0; }catch(error){s.message=error.message;s.error=true;} finally{s.cancelling=false;paint(s,false);} } async function animateView(s,view,turn,generation){ const before=s.view??initial[s.game]; if(s.game!=="cube"||!turn||s.skip||s.restoring||selected!==s.game||reducedMotion){ s.view=view;paint(s);return; } const duration=145;let started=null; await new Promise(resolve=>{ function frame(now){ if(generation!==s.generation)return resolve(); if(started===null)started=now; const p=Math.min(1,(now-started)/duration); if(selected===s.game)safeRender(view,{previousView:before,action:turn,progress:p}); if(p<1&&!s.skip&&selected===s.game)requestAnimationFrame(frame);else resolve(); } requestAnimationFrame(frame); }); if(generation!==s.generation)return; s.view=view;paint(s); } async function processEvent(s,event,generation){ if(event.metrics)s.metrics={...s.metrics,...event.metrics}; if(event.decision)s.decision=event.decision; if(event.message){s.message=pretty(event.message);s.error=event.kind==="error";} if(event.kind==="started"||event.kind==="start"||event.kind==="initial")s.message=""; const frames=Array.isArray(event.frames)?event.frames:[]; if(event.view||frames.length)s.initialReceived=true; if(s.game==="cube"&&frames.length){ paint(s,false); for(const frame of frames){ if(generation!==s.generation)return; const view=frame.view??frame; await animateView(s,view,frame.primitive_turn??view.primitive_turn,generation); } } if(event.view){ s.view=event.view;paint(s); }else if(frames.length&&s.game!=="cube"){ s.view=frames[frames.length-1].view??frames[frames.length-1];paint(s); }else paint(s,false); } async function drain(s){ if(s.draining)return;s.draining=true; const generation=s.generation; try{ while(s.queue.length&&generation===s.generation){ const event=s.queue.shift(); await processEvent(s,event,generation); } }finally{ s.draining=false; if(TERMINAL.has(s.status)&&!s.queue.length)s.skip=false; paint(s,false); } } async function poll(s){ if(s.polling||!s.job||Date.now()s.after){s.queue.push(event);if(Number.isInteger(event.seq))s.after=event.seq;} } s.hasMore=!!data.has_more; if(data.summary)s.summary=data.summary; if(data.status)s.status=data.status; if(TERMINAL.has(s.status)&&s.message.startsWith("Stopping this run"))s.message=""; if(s.message.startsWith("Reconnecting")){s.message="";s.error=false;} if(s.status!=="queued"&&s.message.startsWith("Your run is queued"))s.message=""; // On reload, catch up to the latest recorded state without replaying old turns. if(s.restoring){ for(const event of s.queue.splice(0)){ if(event.metrics)s.metrics={...s.metrics,...event.metrics}; if(event.decision)s.decision=event.decision; if(event.view){s.view=event.view;s.initialReceived=true;} else if(event.frames?.length){s.view=event.frames.at(-1).view??event.frames.at(-1);s.initialReceived=true;} } if(!data.has_more)s.restoring=false; paint(s); }else void drain(s); s.nextPoll=Date.now()+(data.has_more?20:s.status==="queued"?1000:300); paint(s,false); }catch(error){ s.message="Reconnecting to your run… "+error.message;s.error=true;s.nextPoll=Date.now()+2500; if(error.status===403||error.status===404||error.status===410){ s.status="failed";s.restoring=false;s.message="This run is no longer available. Start a new run.";persist({...s,job:null}); } paint(s,false); }finally{s.polling=false;} } async function checkHealth(){ try{ health=await request("/api/arena/status");online=!!health.online; $("connection-text").textContent=online?"GPU connected":"GPU reconnecting"; $("connection").className="connection "+(online?"online":"offline"); }catch{ online=false;$("connection-text").textContent="Connection unavailable";$("connection").className="connection offline"; } controls(states[selected]); } document.querySelectorAll(".game-tab").forEach(tab=>{ tab.addEventListener("click",()=>selectGame(tab.dataset.game)); tab.addEventListener("keydown",event=>{ const games=Object.keys(GAMES);let index=games.indexOf(selected); if(event.key==="ArrowRight")index=(index+1)%games.length; else if(event.key==="ArrowLeft")index=(index+games.length-1)%games.length; else if(event.key==="Home")index=0; else if(event.key==="End")index=games.length-1; else return; event.preventDefault();selectGame(games[index]);$("tab-"+games[index]).focus(); }); }); $("start").addEventListener("click",start);$("stop").addEventListener("click",stop); $("shuffle").addEventListener("click",()=>{ const array=new Uint32Array(1);crypto.getRandomValues(array); states[selected].seed=array[0]%2147483647;$("seed").value=states[selected].seed; }); $("seed").addEventListener("change",()=>{const value=Number($("seed").value);if(Number.isInteger(value))states[selected].seed=value;}); window.addEventListener("hashchange",()=>selectGame(location.hash.slice(1),false)); $("watch-example").addEventListener("click",()=>{ $("example-title").textContent=GAMES[selected].exampleTitle; $("example-video").src=GAMES[selected].example; $("example-dialog").showModal(); }); function closeExample(){$("example-video").pause();$("example-dialog").close();$("example-video").removeAttribute("src");$("example-video").load();} $("close-example").addEventListener("click",closeExample); $("example-dialog").addEventListener("cancel",()=>{$("example-video").pause();}); $("example-dialog").addEventListener("click",event=>{if(event.target===$("example-dialog")){const rect=event.target.getBoundingClientRect();if(event.clientXrect.right||event.clientYrect.bottom)closeExample();}}); let lastWidth=window.innerWidth; window.addEventListener("resize",()=>{if((lastWidth<=500)!==(window.innerWidth<=500)){rendererOptions.compact=window.innerWidth<=500;paint(states[selected]);}lastWidth=window.innerWidth;}); async function init(){ for(const [game,s] of Object.entries(states)){ const saved=safelyRead("jev-arena-"+game); if(saved&&typeof saved.id==="string"&&typeof saved.ticket==="string"&&saved.game===game){ s.job=saved;s.seed=Number.isInteger(saved.seed)?saved.seed:GAMES[game].seed;s.status="running";s.restoring=true; } } selectGame(selected); try{initial=await request("/arena-initial.json");paint(states[selected]);}catch{ /* The first live view replaces the opening preview. */ } await checkHealth(); setInterval(checkHealth,7000); setInterval(()=>{for(const s of Object.values(states))void poll(s);},150); for(const s of Object.values(states))void poll(s); } void init(); })();