Spaces:
Running
Running
File size: 15,819 Bytes
6a726cf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | /* Shared, deterministic Canvas renderers. No network access or model execution. */
(function(global) {
"use strict";
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);
|