Spaces:
Running
Running
Download arena-renderer.js from autotrust/JEV-27B-Demo: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/spaces/autotrust/JEV-27B-Demo/resolve/main/arena-renderer.js
- Command line
-
hf download hf://spaces/autotrust/JEV-27B-Demo/arena-renderer.js
-
curl -L -o arena-renderer.js https://huggingface.co/spaces/autotrust/JEV-27B-Demo/resolve/main/arena-renderer.js
15.8 kB
| /* Shared, deterministic Canvas renderers. No network access or model execution. */ | |
| (function(global) { | |
| ; | |
| const C = {bg:"#ffffff",panel:"#f4f6f8",line:"#cbd4df",cream:"#273448",muted:"#697586",gold:"#a87821",mint:"#3b7db6"}; | |
| // Coordinates and turn signs mirror arena/games/cube.py. Every sticker is read | |
| // from the recorded payload, including faces revealed during a layer turn. | |
| const CUBE_FACES={ | |
| U:{normal:[0,1,0],right:[1,0,0],down:[0,0,1]}, | |
| D:{normal:[0,-1,0],right:[1,0,0],down:[0,0,-1]}, | |
| L:{normal:[-1,0,0],right:[0,0,1],down:[0,-1,0]}, | |
| R:{normal:[1,0,0],right:[0,0,-1],down:[0,-1,0]}, | |
| F:{normal:[0,0,1],right:[1,0,0],down:[0,-1,0]}, | |
| B:{normal:[0,0,-1],right:[-1,0,0],down:[0,-1,0]} | |
| }; | |
| const cubeAdd=(a,b)=>a.map((v,i)=>v+b[i]); | |
| const cubeScale=(a,s)=>a.map(v=>v*s); | |
| const cubeDot=(a,b)=>a.reduce((s,v,i)=>s+v*b[i],0); | |
| function cubePosition(face,row,column){const f=CUBE_FACES[face];return cubeAdd(f.normal,cubeAdd(cubeScale(f.right,column-1),cubeScale(f.down,row-1)));} | |
| function cubeRotate(v,axis,angle){ | |
| const [x,y,z]=v,c=Math.cos(angle),s=Math.sin(angle); | |
| return axis===0?[x,y*c-z*s,y*s+z*c]:axis===1?[x*c+z*s,y,-x*s+z*c]:[x*c-y*s,x*s+y*c,z]; | |
| } | |
| function cubeTurn(action){ | |
| if(typeof action!=="string"||!/^[UDLRFB]'?$/.test(action))return null; | |
| const n=CUBE_FACES[action[0]].normal,axis=n.findIndex(v=>v!==0),sign=n[axis]; | |
| return {axis,sign,angle:(action.endsWith("'")?sign:-sign)*Math.PI/2}; | |
| } | |
| function cubeStickers(faces){ | |
| if(!faces)return null; | |
| const result=[]; | |
| for(const [face,f] of Object.entries(CUBE_FACES)){ | |
| const rows=faces[face]; | |
| if(!Array.isArray(rows)||rows.length!==3||rows.some(r=>!Array.isArray(r)||r.length!==3))return null; | |
| rows.forEach((row,r)=>row.forEach((color,c)=>result.push({position:cubePosition(face,r,c),normal:f.normal,color,face,row:r,column:c}))); | |
| } | |
| return result; | |
| } | |
| function cubeKey(position,normal){return [...position,...normal].map(v=>Math.round(v)).join(",");} | |
| function cubeTransitionMatches(before,after,action){ | |
| const turn=cubeTurn(action),source=cubeStickers(before?.faces),target=cubeStickers(after?.faces); | |
| if(!turn||!source||!target)return false; | |
| const actual=new Map(target.map(s=>[cubeKey(s.position,s.normal),s.color])); | |
| return source.every(s=>{ | |
| const moving=s.position[turn.axis]===turn.sign; | |
| const p=moving?cubeRotate(s.position,turn.axis,turn.angle):s.position; | |
| const n=moving?cubeRotate(s.normal,turn.axis,turn.angle):s.normal; | |
| return actual.get(cubeKey(p,n))===s.color; | |
| }); | |
| } | |
| function create(canvas, options={}) { | |
| const ctx=canvas.getContext("2d"); | |
| let renderSequence=0; | |
| const nesImages=new Map(); | |
| const compactMode=()=>options.compact ?? (window.innerWidth<=600); | |
| // All drawings come directly from the active game's renderer payload. | |
| function rect(x,y,w,h,fill,r=0,stroke=null){ctx.beginPath();ctx.roundRect(x,y,w,h,r);if(fill){ctx.fillStyle=fill;ctx.fill();}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=1;ctx.stroke();}} | |
| function text(value,x,y,size=16,color=C.cream,align="left",serif=false){ctx.fillStyle=color;ctx.font=`${size}px ${serif?"Georgia":"system-ui, sans-serif"}`;ctx.textAlign=align;ctx.textBaseline="middle";ctx.fillText(String(value),x,y);} | |
| function line(x1,y1,x2,y2,color,width=1){ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.strokeStyle=color;ctx.lineWidth=width;ctx.stroke();} | |
| // Optional replayContext for a recorded cube turn: | |
| // { previousView: viewBefore, action: "R'", progress: 0..1 }. | |
| // The supplied view is viewAfter. Invalid transitions remain static; this API | |
| // never advances a game or reads wall-clock time, so seeking is deterministic. | |
| function render(view, replayContext=null) { | |
| const sequence=++renderSequence; | |
| const compact=compactMode(),width=compact?600:1000,height=compact?650:600;canvas.width=width;canvas.height=height;ctx.clearRect(0,0,width,height); | |
| const grad=ctx.createRadialGradient(500,280,50,500,300,600);grad.addColorStop(0,"#ffffff");grad.addColorStop(1,"#ffffff");ctx.fillStyle=grad;ctx.fillRect(0,0,width,height); | |
| if(view?.kind!=="cube"){ctx.globalAlpha=.35;for(let x=20;x<1000;x+=25)for(let y=20;y<600;y+=25){ctx.fillStyle=C.line;ctx.fillRect(x,y,1,1);}ctx.globalAlpha=1;} | |
| if(!view)return; | |
| if(compact)ctx.translate(-200,25); | |
| if(view.kind==="tetris")drawTetris(view); | |
| else if(view.kind==="cube")drawCube(view,replayContext); | |
| else if(view.kind==="obstacles"||view.kind==="sokoban")drawGrid(view); | |
| else if(view.kind==="mario"){ | |
| if(view.engine==="nes")return drawNesMario(view,sequence); | |
| drawMario(view); | |
| } | |
| } | |
| function tileBlock(x,y,size,color,alpha=1) {ctx.globalAlpha=alpha;rect(x+1.5,y+1.5,size-3,size-3,color,3);ctx.globalAlpha=alpha*.25;rect(x+4,y+4,size-8,3,"#fff",1);ctx.globalAlpha=1;} | |
| function drawTetris(v) { | |
| const s=25,x0=350,y0=45;rect(x0-10,y0-10,270,520,"#ffffff",8,"#cdd4df"); | |
| v.board.forEach((row,y)=>row.forEach((piece,x)=>{rect(x0+x*s+1,y0+y*s+1,s-2,s-2,"#f1f4f7",1);if(piece)tileBlock(x0+x*s,y0+y*s,s,v.colors[piece]);})); | |
| if(v.ghost)v.ghost.cells.forEach(([x,y])=>{if(y>=0)rect(x0+x*s+3,y0+y*s+3,s-6,s-6,null,3,"#a8782177");}); | |
| if(v.active)v.active.cells.forEach(([x,y])=>{if(y>=0)tileBlock(x0+x*s,y0+y*s,s,v.colors[v.active.piece]);}); | |
| const labelX=compactMode()?310:285; | |
| text("LINE CLEAR",labelX,110,13,C.muted,"right");text(`${v.lines} / ${v.goal_lines}`,labelX,153,compactMode()?36:44,C.gold,"right",true); | |
| text("SCORE",labelX,229,13,C.muted,"right");text(v.score,labelX,269,30,C.cream,"right",true); | |
| text("NEXT",665,87,13,C.muted);v.next.slice(0,3).forEach((piece,i)=>{rect(655,115+i*113,143,94,"#f7f8fa",6,"#dce1e7");(v.piece_shapes[piece]||[]).forEach(([x,y])=>tileBlock(677+x*24,137+i*113+y*24,24,v.colors[piece]));}); | |
| text(v.realtime?`${v.speed_multiplier.toFixed(1)}× FALL SPEED · ${Math.round(v.gravity_ms)} ms / row`:"Find the gap. Make it fit.",500,575,14,v.realtime?C.gold:C.muted,"center"); | |
| } | |
| function drawCube(v,replayContext) { | |
| let source=v,turn=null,angle=0,progress=1; | |
| if(replayContext&&Number.isFinite(replayContext.progress) | |
| &&cubeTransitionMatches(replayContext.previousView,v,replayContext.action)){ | |
| progress=Math.max(0,Math.min(1,replayContext.progress)); | |
| // Exact endpoints use their recorded state, avoiding floating-point seams. | |
| if(progress<1){source=replayContext.previousView;turn=cubeTurn(replayContext.action);angle=turn.angle*(progress*progress*(3-2*progress));} | |
| } | |
| const stickers=cubeStickers(source.faces); | |
| if(!stickers){text("Cube state unavailable",500,300,20,C.muted,"center");return;} | |
| const stickerMap=new Map(stickers.map(s=>[cubeKey(s.position,s.normal),s.color])); | |
| const eye=[1,1,1].map(n=>n/Math.sqrt(3)); | |
| const right=[Math.SQRT1_2,0,-Math.SQRT1_2],up=[-1/Math.sqrt(6),2/Math.sqrt(6),-1/Math.sqrt(6)]; | |
| const scale=86,center=[500,286],polygons=[]; | |
| const project=p=>[center[0]+scale*cubeDot(p,right),center[1]-scale*cubeDot(p,up)]; | |
| const transform=(p,moving)=>moving?cubeRotate(p,turn.axis,angle):p; | |
| const corners=(center,u,d,half)=>[[-1,-1],[1,-1],[1,1],[-1,1]].map(([a,b])=>cubeAdd(center,cubeAdd(cubeScale(u,a*half),cubeScale(d,b*half)))); | |
| // Paint all solid cubies, not only the front stickers. During a quarter turn | |
| // this exposes the genuine plastic underside of the moving slice. | |
| for(let x=-1;x<=1;x++)for(let y=-1;y<=1;y++)for(let z=-1;z<=1;z++){ | |
| const position=[x,y,z],moving=!!turn&&position[turn.axis]===turn.sign; | |
| for(const face of Object.values(CUBE_FACES)){ | |
| const normal=transform(face.normal,moving); | |
| if(cubeDot(normal,eye)<=1e-7)continue; | |
| const middle=transform(cubeAdd(position,cubeScale(face.normal,.487)),moving); | |
| const points=corners(cubeAdd(position,cubeScale(face.normal,.487)),face.right,face.down,.487).map(p=>transform(p,moving)); | |
| const color=stickerMap.get(cubeKey(position,face.normal)); | |
| polygons.push({depth:cubeDot(middle,eye),points:points.map(project),normal, | |
| sticker:color===undefined?null:{color:source.colors[color],points:corners(cubeAdd(position,cubeScale(face.normal,.489)),face.right,face.down,.415).map(p=>project(transform(p,moving)))}}); | |
| } | |
| } | |
| ctx.save();ctx.translate(500,515);ctx.scale(1,.12); | |
| const shadow=ctx.createRadialGradient(0,0,10,0,0,200); | |
| shadow.addColorStop(0,"#27344824");shadow.addColorStop(1,"#27344800"); | |
| ctx.fillStyle=shadow;ctx.fillRect(-220,-220,440,440);ctx.restore(); | |
| function polygon(points,roundness=0){ | |
| ctx.beginPath(); | |
| points.forEach((p,i)=>{ | |
| const prev=points[(i+points.length-1)%points.length],next=points[(i+1)%points.length]; | |
| const a=p.map((v,j)=>v+(prev[j]-v)*roundness),b=p.map((v,j)=>v+(next[j]-v)*roundness); | |
| if(i===0)ctx.moveTo(...a);else ctx.lineTo(...a); | |
| ctx.quadraticCurveTo(...p,...b); | |
| });ctx.closePath(); | |
| } | |
| polygons.sort((a,b)=>a.depth-b.depth).forEach(p=>{ | |
| const top=Math.max(0,p.normal[1]),side=Math.max(0,p.normal[0]); | |
| polygon(p.points,.025);ctx.fillStyle=top>.5?"#293544":side>.5?"#15202e":"#1d2938";ctx.fill(); | |
| ctx.strokeStyle="#0c17253b";ctx.lineWidth=.65;ctx.stroke(); | |
| if(p.sticker){ | |
| polygon(p.sticker.points,.095);ctx.fillStyle=p.sticker.color;ctx.fill(); | |
| // Gentle material highlights retain each recorded sticker's base color. | |
| const ys=p.sticker.points.map(q=>q[1]),shine=ctx.createLinearGradient(0,Math.min(...ys),0,Math.max(...ys)); | |
| shine.addColorStop(0,`rgba(255,255,255,${.12+.08*top})`);shine.addColorStop(.5,"rgba(255,255,255,0)");shine.addColorStop(1,`rgba(0,0,0,${.035+.035*side})`); | |
| ctx.fillStyle=shine;ctx.fill();ctx.strokeStyle="#ffffff38";ctx.lineWidth=.7;ctx.stroke(); | |
| } | |
| }); | |
| text("RUBIK’S CUBE",500,35,12,C.muted,"center"); | |
| text(`${source.solved_faces} of 6 faces aligned`,500,554,18,C.cream,"center"); | |
| if(turn&&progress>0&&progress<1){rect(466,575,68,24,"#f0f4f7",12);text(replayContext.action,500,587,15,C.cream,"center");} | |
| } | |
| function drawGrid(v) { | |
| if(v.campaign)text(`LEVEL ${v.campaign.level} / ${v.campaign.total_levels} · ${v.campaign.completed_levels} CLEARED`,500,45,18,C.gold,"center"); | |
| const size=Math.min(61,490/v.height,690/v.width),left=(1000-v.width*size)/2,top=(600-v.height*size)/2; | |
| rect(left-12,top-12,v.width*size+24,v.height*size+24,"#ffffff",10,"#cbd4df"); | |
| for(let y=0;y<v.height;y++)for(let x=0;x<v.width;x++)rect(left+x*size+1,top+y*size+1,size-2,size-2,"#f1f4f7",3); | |
| v.walls.forEach(([x,y])=>{rect(left+x*size+1,top+y*size+1,size-2,size-2,"#91a0b2",3);line(left+x*size+6,top+y*size+6,left+(x+1)*size-6,top+y*size+6,"#bac7d5",1);}); | |
| if(v.kind==="sokoban") { | |
| v.goals.forEach(([x,y])=>{const cx=left+(x+.5)*size,cy=top+(y+.5)*size;ctx.beginPath();ctx.arc(cx,cy,size*.22,0,Math.PI*2);ctx.strokeStyle=C.gold;ctx.lineWidth=2;ctx.stroke();ctx.fillStyle="#a8782124";ctx.fill();}); | |
| const goals=new Set(v.goals.map(p=>p.join(","))); | |
| v.boxes.forEach(([x,y])=>{const px=left+x*size+5,py=top+y*size+5,s=size-10,on=goals.has([x,y].join(","));rect(px,py,s,s,on?"#a6bb7f":"#c79e58",4,on?"#d4e4a7":"#e1c17f");line(px+5,py+5,px+s-5,py+s-5,"#6a6838",2);line(px+s-5,py+5,px+5,py+s-5,"#6a6838",2);}); | |
| } else { | |
| const [gx,gy]=v.goal;rect(left+gx*size+6,top+gy*size+6,size-12,size-12,C.gold,6);text("⚑",left+(gx+.5)*size,top+(gy+.5)*size,size*.52,C.bg,"center"); | |
| v.obstacles.forEach(o=>{const [x,y]=o.position,cx=left+(x+.5)*size,cy=top+(y+.5)*size;ctx.save();ctx.translate(cx,cy);ctx.rotate(Math.PI/4);rect(-size*.3,-size*.3,size*.6,size*.6,"#dc9278",4);ctx.restore();text(o.direction==="right"?"→":"←",cx,cy,Math.max(13,size*.4),"#4b3025","center");}); | |
| } | |
| const [px,py]=v.player,cx=left+(px+.5)*size,cy=top+(py+.5)*size;ctx.beginPath();ctx.arc(cx,cy,size*.28,0,Math.PI*2);ctx.fillStyle=C.cream;ctx.fill();ctx.strokeStyle="#ffffff";ctx.lineWidth=3;ctx.stroke();ctx.beginPath();ctx.arc(cx+size*.06,cy-size*.07,size*.055,0,Math.PI*2);ctx.fillStyle=C.bg;ctx.fill(); | |
| } | |
| function drawNesMario(v,sequence) { | |
| const uri=v.screen?.png; | |
| if(typeof uri!=="string"||!uri.startsWith("data:image/png;base64,")) | |
| return Promise.reject(new Error("Original Mario recording is missing its native PNG frame.")); | |
| let pending=nesImages.get(uri); | |
| if(!pending){ | |
| pending=new Promise((resolve,reject)=>{ | |
| const nativeImage=new Image(); | |
| nativeImage.onload=()=>resolve(nativeImage); | |
| nativeImage.onerror=()=>reject(new Error("Could not decode the original Mario frame.")); | |
| nativeImage.src=uri; | |
| }); | |
| nesImages.set(uri,pending); | |
| if(nesImages.size>24)nesImages.delete(nesImages.keys().next().value); | |
| } | |
| return pending.then(nativeImage=>{ | |
| // Live playback may have requested a newer frame while PNG decoding ran. | |
| if(sequence!==renderSequence)return; | |
| const scale=2,left=500-nativeImage.width*scale/2,top=58; | |
| rect(left-7,top-7,nativeImage.width*scale+14,nativeImage.height*scale+14,"#ffffff",8,"#cbd4df"); | |
| ctx.imageSmoothingEnabled=false; | |
| ctx.drawImage(nativeImage,left,top,nativeImage.width*scale,nativeImage.height*scale); | |
| text("SUPER MARIO BROS.",500,28,13,C.muted,"center"); | |
| text(`WORLD ${v.hud?.world??1}-${v.hud?.stage??1}`,500,574,15,C.cream,"center"); | |
| }); | |
| } | |
| function drawMario(v) { | |
| if(!v.camera||!v.tiles){text("Mario AI Framework is preparing the level…",500,300,22,C.cream,"center",true);return;} | |
| const scale=1.98,w=v.camera.width*scale,h=v.camera.height*scale,left=(1000-w)/2,top=(600-h)/2,tile=v.tile_size||16; | |
| ctx.save();ctx.beginPath();ctx.roundRect(left-6,top-6,w+12,h+12,8);ctx.fillStyle="#ffffff";ctx.fill();ctx.strokeStyle="#cbd4df";ctx.stroke();ctx.beginPath();ctx.rect(left,top,w,h);ctx.clip();ctx.translate(left,top);ctx.scale(scale,scale); | |
| const sky=ctx.createLinearGradient(0,0,0,v.camera.height);sky.addColorStop(0,"#cde8ff");sky.addColorStop(1,"#f5fbff");ctx.fillStyle=sky;ctx.fillRect(0,0,v.camera.width,v.camera.height); | |
| // Decorative sky follows the camera; every playable tile comes from the engine. | |
| for(let i=0;i<5;i++){const x=(i*97-v.camera.x*.15)%340;rect(x,30+(i%3)*22,27,7,"#e7e4c4",4);rect(x+6,25+(i%3)*22,13,12,"#e7e4c4",4);} | |
| const palettes={ground:["#9b8152","#a0ac6b"],pipe:["#507e40","#83a352"],question:["#d2ad4d","#f0cf66"],brick:["#aa704e","#d29463"],used:["#947d58","#baa377"],platform:["#6f8258","#a7b075"]}; | |
| v.tiles.forEach(t=>{const x=t.x*tile-v.camera.x,y=t.y*tile-(v.camera.y||0);if(t.kind==="coin"){ctx.beginPath();ctx.ellipse(x+8,y+8,3,6,0,0,Math.PI*2);ctx.fillStyle="#f1d163";ctx.fill();return;}if(t.kind==="flag"){rect(x+7,y,2,16,"#ebebd8");ctx.beginPath();ctx.moveTo(x+7,y+2);ctx.lineTo(x+15,y+5);ctx.lineTo(x+7,y+9);ctx.fillStyle="#a87821";ctx.fill();return;}const colors=palettes[t.kind]||palettes.ground;rect(x,y,tile,tile,colors[0]);rect(x+1,y+1,tile-2,3,colors[1]);if(t.kind==="question")text("?",x+8,y+9,12,"#604c28","center");if(t.kind==="brick"){line(x,y+8,x+16,y+8,"#704d35");line(x+8,y,x+8,y+8,"#704d35");line(x+4,y+8,x+4,y+16,"#704d35");}if(t.kind==="pipe"){rect(x+3,y,3,tile,"#99b16b");}}); | |
| (v.enemies||[]).forEach(e=>{const x=e.x-v.camera.x-e.width/2,y=e.y-(v.camera.y||0)-e.height;rect(x,y,e.width,e.height,"#855132",4);rect(x+2,y+3,3,4,"#f5e7ca");rect(x+e.width-5,y+3,3,4,"#f5e7ca");rect(x-1,y+e.height-3,e.width+2,3,"#45382b",1);}); | |
| const p=v.player;if(p){const x=p.x-v.camera.x-p.width/2,y=p.y-(v.camera.y||0)-p.height,pw=p.width,ph=p.height;rect(x,y+ph*.35,pw,ph*.5,"#497799",1);rect(x+1,y+ph*.1,pw-2,ph*.38,"#e2b886",1);rect(x-1,y,pw+2,ph*.18,"#b5533f",1);rect(x+(p.facing<0?1:pw-3),y+ph*.2,2,3,"#3a3025");rect(x-1,y+ph-3,pw/2,3,"#46392e",1);rect(x+pw/2+1,y+ph-3,pw/2,3,"#46392e",1);} | |
| ctx.restore();if(!compactMode()){text("MARIO AI FRAMEWORK",left-29,85,12,C.gold,"right");text("Reach the flag",left-29,112,15,C.muted,"right");text("COINS",left+w+29,85,12,C.muted);text(v.hud?.coins??0,left+w+29,120,30,C.cream,"left",true);} | |
| } | |
| return {render}; | |
| } | |
| global.ArenaRenderer={create}; | |
| })(window); | |