Spaces:
Running
Running
Create arena-renderer.js
Browse files- arena-renderer.js +210 -0
arena-renderer.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Shared, deterministic Canvas renderers. No network access or model execution. */
|
| 2 |
+
(function(global) {
|
| 3 |
+
"use strict";
|
| 4 |
+
const C = {bg:"#ffffff",panel:"#f4f6f8",line:"#cbd4df",cream:"#273448",muted:"#697586",gold:"#a87821",mint:"#3b7db6"};
|
| 5 |
+
// Coordinates and turn signs mirror arena/games/cube.py. Every sticker is read
|
| 6 |
+
// from the recorded payload, including faces revealed during a layer turn.
|
| 7 |
+
const CUBE_FACES={
|
| 8 |
+
U:{normal:[0,1,0],right:[1,0,0],down:[0,0,1]},
|
| 9 |
+
D:{normal:[0,-1,0],right:[1,0,0],down:[0,0,-1]},
|
| 10 |
+
L:{normal:[-1,0,0],right:[0,0,1],down:[0,-1,0]},
|
| 11 |
+
R:{normal:[1,0,0],right:[0,0,-1],down:[0,-1,0]},
|
| 12 |
+
F:{normal:[0,0,1],right:[1,0,0],down:[0,-1,0]},
|
| 13 |
+
B:{normal:[0,0,-1],right:[-1,0,0],down:[0,-1,0]}
|
| 14 |
+
};
|
| 15 |
+
const cubeAdd=(a,b)=>a.map((v,i)=>v+b[i]);
|
| 16 |
+
const cubeScale=(a,s)=>a.map(v=>v*s);
|
| 17 |
+
const cubeDot=(a,b)=>a.reduce((s,v,i)=>s+v*b[i],0);
|
| 18 |
+
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)));}
|
| 19 |
+
function cubeRotate(v,axis,angle){
|
| 20 |
+
const [x,y,z]=v,c=Math.cos(angle),s=Math.sin(angle);
|
| 21 |
+
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];
|
| 22 |
+
}
|
| 23 |
+
function cubeTurn(action){
|
| 24 |
+
if(typeof action!=="string"||!/^[UDLRFB]'?$/.test(action))return null;
|
| 25 |
+
const n=CUBE_FACES[action[0]].normal,axis=n.findIndex(v=>v!==0),sign=n[axis];
|
| 26 |
+
return {axis,sign,angle:(action.endsWith("'")?sign:-sign)*Math.PI/2};
|
| 27 |
+
}
|
| 28 |
+
function cubeStickers(faces){
|
| 29 |
+
if(!faces)return null;
|
| 30 |
+
const result=[];
|
| 31 |
+
for(const [face,f] of Object.entries(CUBE_FACES)){
|
| 32 |
+
const rows=faces[face];
|
| 33 |
+
if(!Array.isArray(rows)||rows.length!==3||rows.some(r=>!Array.isArray(r)||r.length!==3))return null;
|
| 34 |
+
rows.forEach((row,r)=>row.forEach((color,c)=>result.push({position:cubePosition(face,r,c),normal:f.normal,color,face,row:r,column:c})));
|
| 35 |
+
}
|
| 36 |
+
return result;
|
| 37 |
+
}
|
| 38 |
+
function cubeKey(position,normal){return [...position,...normal].map(v=>Math.round(v)).join(",");}
|
| 39 |
+
function cubeTransitionMatches(before,after,action){
|
| 40 |
+
const turn=cubeTurn(action),source=cubeStickers(before?.faces),target=cubeStickers(after?.faces);
|
| 41 |
+
if(!turn||!source||!target)return false;
|
| 42 |
+
const actual=new Map(target.map(s=>[cubeKey(s.position,s.normal),s.color]));
|
| 43 |
+
return source.every(s=>{
|
| 44 |
+
const moving=s.position[turn.axis]===turn.sign;
|
| 45 |
+
const p=moving?cubeRotate(s.position,turn.axis,turn.angle):s.position;
|
| 46 |
+
const n=moving?cubeRotate(s.normal,turn.axis,turn.angle):s.normal;
|
| 47 |
+
return actual.get(cubeKey(p,n))===s.color;
|
| 48 |
+
});
|
| 49 |
+
}
|
| 50 |
+
function create(canvas, options={}) {
|
| 51 |
+
const ctx=canvas.getContext("2d");
|
| 52 |
+
let renderSequence=0;
|
| 53 |
+
const nesImages=new Map();
|
| 54 |
+
const compactMode=()=>options.compact ?? (window.innerWidth<=600);
|
| 55 |
+
// All drawings come directly from the active game's renderer payload.
|
| 56 |
+
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();}}
|
| 57 |
+
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);}
|
| 58 |
+
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();}
|
| 59 |
+
// Optional replayContext for a recorded cube turn:
|
| 60 |
+
// { previousView: viewBefore, action: "R'", progress: 0..1 }.
|
| 61 |
+
// The supplied view is viewAfter. Invalid transitions remain static; this API
|
| 62 |
+
// never advances a game or reads wall-clock time, so seeking is deterministic.
|
| 63 |
+
function render(view, replayContext=null) {
|
| 64 |
+
const sequence=++renderSequence;
|
| 65 |
+
const compact=compactMode(),width=compact?600:1000,height=compact?650:600;canvas.width=width;canvas.height=height;ctx.clearRect(0,0,width,height);
|
| 66 |
+
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);
|
| 67 |
+
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;}
|
| 68 |
+
if(!view)return;
|
| 69 |
+
if(compact)ctx.translate(-200,25);
|
| 70 |
+
if(view.kind==="tetris")drawTetris(view);
|
| 71 |
+
else if(view.kind==="cube")drawCube(view,replayContext);
|
| 72 |
+
else if(view.kind==="obstacles"||view.kind==="sokoban")drawGrid(view);
|
| 73 |
+
else if(view.kind==="mario"){
|
| 74 |
+
if(view.engine==="nes")return drawNesMario(view,sequence);
|
| 75 |
+
drawMario(view);
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
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;}
|
| 79 |
+
function drawTetris(v) {
|
| 80 |
+
const s=25,x0=350,y0=45;rect(x0-10,y0-10,270,520,"#ffffff",8,"#cdd4df");
|
| 81 |
+
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]);}));
|
| 82 |
+
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");});
|
| 83 |
+
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]);});
|
| 84 |
+
const labelX=compactMode()?310:285;
|
| 85 |
+
text("LINE CLEAR",labelX,110,13,C.muted,"right");text(`${v.lines} / ${v.goal_lines}`,labelX,153,compactMode()?36:44,C.gold,"right",true);
|
| 86 |
+
text("SCORE",labelX,229,13,C.muted,"right");text(v.score,labelX,269,30,C.cream,"right",true);
|
| 87 |
+
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]));});
|
| 88 |
+
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");
|
| 89 |
+
}
|
| 90 |
+
function drawCube(v,replayContext) {
|
| 91 |
+
let source=v,turn=null,angle=0,progress=1;
|
| 92 |
+
if(replayContext&&Number.isFinite(replayContext.progress)
|
| 93 |
+
&&cubeTransitionMatches(replayContext.previousView,v,replayContext.action)){
|
| 94 |
+
progress=Math.max(0,Math.min(1,replayContext.progress));
|
| 95 |
+
// Exact endpoints use their recorded state, avoiding floating-point seams.
|
| 96 |
+
if(progress<1){source=replayContext.previousView;turn=cubeTurn(replayContext.action);angle=turn.angle*(progress*progress*(3-2*progress));}
|
| 97 |
+
}
|
| 98 |
+
const stickers=cubeStickers(source.faces);
|
| 99 |
+
if(!stickers){text("Cube state unavailable",500,300,20,C.muted,"center");return;}
|
| 100 |
+
const stickerMap=new Map(stickers.map(s=>[cubeKey(s.position,s.normal),s.color]));
|
| 101 |
+
const eye=[1,1,1].map(n=>n/Math.sqrt(3));
|
| 102 |
+
const right=[Math.SQRT1_2,0,-Math.SQRT1_2],up=[-1/Math.sqrt(6),2/Math.sqrt(6),-1/Math.sqrt(6)];
|
| 103 |
+
const scale=86,center=[500,286],polygons=[];
|
| 104 |
+
const project=p=>[center[0]+scale*cubeDot(p,right),center[1]-scale*cubeDot(p,up)];
|
| 105 |
+
const transform=(p,moving)=>moving?cubeRotate(p,turn.axis,angle):p;
|
| 106 |
+
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))));
|
| 107 |
+
// Paint all solid cubies, not only the front stickers. During a quarter turn
|
| 108 |
+
// this exposes the genuine plastic underside of the moving slice.
|
| 109 |
+
for(let x=-1;x<=1;x++)for(let y=-1;y<=1;y++)for(let z=-1;z<=1;z++){
|
| 110 |
+
const position=[x,y,z],moving=!!turn&&position[turn.axis]===turn.sign;
|
| 111 |
+
for(const face of Object.values(CUBE_FACES)){
|
| 112 |
+
const normal=transform(face.normal,moving);
|
| 113 |
+
if(cubeDot(normal,eye)<=1e-7)continue;
|
| 114 |
+
const middle=transform(cubeAdd(position,cubeScale(face.normal,.487)),moving);
|
| 115 |
+
const points=corners(cubeAdd(position,cubeScale(face.normal,.487)),face.right,face.down,.487).map(p=>transform(p,moving));
|
| 116 |
+
const color=stickerMap.get(cubeKey(position,face.normal));
|
| 117 |
+
polygons.push({depth:cubeDot(middle,eye),points:points.map(project),normal,
|
| 118 |
+
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)))}});
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
ctx.save();ctx.translate(500,515);ctx.scale(1,.12);
|
| 122 |
+
const shadow=ctx.createRadialGradient(0,0,10,0,0,200);
|
| 123 |
+
shadow.addColorStop(0,"#27344824");shadow.addColorStop(1,"#27344800");
|
| 124 |
+
ctx.fillStyle=shadow;ctx.fillRect(-220,-220,440,440);ctx.restore();
|
| 125 |
+
function polygon(points,roundness=0){
|
| 126 |
+
ctx.beginPath();
|
| 127 |
+
points.forEach((p,i)=>{
|
| 128 |
+
const prev=points[(i+points.length-1)%points.length],next=points[(i+1)%points.length];
|
| 129 |
+
const a=p.map((v,j)=>v+(prev[j]-v)*roundness),b=p.map((v,j)=>v+(next[j]-v)*roundness);
|
| 130 |
+
if(i===0)ctx.moveTo(...a);else ctx.lineTo(...a);
|
| 131 |
+
ctx.quadraticCurveTo(...p,...b);
|
| 132 |
+
});ctx.closePath();
|
| 133 |
+
}
|
| 134 |
+
polygons.sort((a,b)=>a.depth-b.depth).forEach(p=>{
|
| 135 |
+
const top=Math.max(0,p.normal[1]),side=Math.max(0,p.normal[0]);
|
| 136 |
+
polygon(p.points,.025);ctx.fillStyle=top>.5?"#293544":side>.5?"#15202e":"#1d2938";ctx.fill();
|
| 137 |
+
ctx.strokeStyle="#0c17253b";ctx.lineWidth=.65;ctx.stroke();
|
| 138 |
+
if(p.sticker){
|
| 139 |
+
polygon(p.sticker.points,.095);ctx.fillStyle=p.sticker.color;ctx.fill();
|
| 140 |
+
// Gentle material highlights retain each recorded sticker's base color.
|
| 141 |
+
const ys=p.sticker.points.map(q=>q[1]),shine=ctx.createLinearGradient(0,Math.min(...ys),0,Math.max(...ys));
|
| 142 |
+
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})`);
|
| 143 |
+
ctx.fillStyle=shine;ctx.fill();ctx.strokeStyle="#ffffff38";ctx.lineWidth=.7;ctx.stroke();
|
| 144 |
+
}
|
| 145 |
+
});
|
| 146 |
+
text("RUBIK’S CUBE",500,35,12,C.muted,"center");
|
| 147 |
+
text(`${source.solved_faces} of 6 faces aligned`,500,554,18,C.cream,"center");
|
| 148 |
+
if(turn&&progress>0&&progress<1){rect(466,575,68,24,"#f0f4f7",12);text(replayContext.action,500,587,15,C.cream,"center");}
|
| 149 |
+
}
|
| 150 |
+
function drawGrid(v) {
|
| 151 |
+
if(v.campaign)text(`LEVEL ${v.campaign.level} / ${v.campaign.total_levels} · ${v.campaign.completed_levels} CLEARED`,500,45,18,C.gold,"center");
|
| 152 |
+
const size=Math.min(61,490/v.height,690/v.width),left=(1000-v.width*size)/2,top=(600-v.height*size)/2;
|
| 153 |
+
rect(left-12,top-12,v.width*size+24,v.height*size+24,"#ffffff",10,"#cbd4df");
|
| 154 |
+
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);
|
| 155 |
+
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);});
|
| 156 |
+
if(v.kind==="sokoban") {
|
| 157 |
+
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();});
|
| 158 |
+
const goals=new Set(v.goals.map(p=>p.join(",")));
|
| 159 |
+
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);});
|
| 160 |
+
} else {
|
| 161 |
+
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");
|
| 162 |
+
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");});
|
| 163 |
+
}
|
| 164 |
+
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();
|
| 165 |
+
}
|
| 166 |
+
function drawNesMario(v,sequence) {
|
| 167 |
+
const uri=v.screen?.png;
|
| 168 |
+
if(typeof uri!=="string"||!uri.startsWith("data:image/png;base64,"))
|
| 169 |
+
return Promise.reject(new Error("Original Mario recording is missing its native PNG frame."));
|
| 170 |
+
let pending=nesImages.get(uri);
|
| 171 |
+
if(!pending){
|
| 172 |
+
pending=new Promise((resolve,reject)=>{
|
| 173 |
+
const nativeImage=new Image();
|
| 174 |
+
nativeImage.onload=()=>resolve(nativeImage);
|
| 175 |
+
nativeImage.onerror=()=>reject(new Error("Could not decode the original Mario frame."));
|
| 176 |
+
nativeImage.src=uri;
|
| 177 |
+
});
|
| 178 |
+
nesImages.set(uri,pending);
|
| 179 |
+
if(nesImages.size>24)nesImages.delete(nesImages.keys().next().value);
|
| 180 |
+
}
|
| 181 |
+
return pending.then(nativeImage=>{
|
| 182 |
+
// Live playback may have requested a newer frame while PNG decoding ran.
|
| 183 |
+
if(sequence!==renderSequence)return;
|
| 184 |
+
const scale=2,left=500-nativeImage.width*scale/2,top=58;
|
| 185 |
+
rect(left-7,top-7,nativeImage.width*scale+14,nativeImage.height*scale+14,"#ffffff",8,"#cbd4df");
|
| 186 |
+
ctx.imageSmoothingEnabled=false;
|
| 187 |
+
ctx.drawImage(nativeImage,left,top,nativeImage.width*scale,nativeImage.height*scale);
|
| 188 |
+
text("SUPER MARIO BROS.",500,28,13,C.muted,"center");
|
| 189 |
+
text(`WORLD ${v.hud?.world??1}-${v.hud?.stage??1}`,500,574,15,C.cream,"center");
|
| 190 |
+
});
|
| 191 |
+
}
|
| 192 |
+
function drawMario(v) {
|
| 193 |
+
if(!v.camera||!v.tiles){text("Mario AI Framework is preparing the level…",500,300,22,C.cream,"center",true);return;}
|
| 194 |
+
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;
|
| 195 |
+
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);
|
| 196 |
+
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);
|
| 197 |
+
// Decorative sky follows the camera; every playable tile comes from the engine.
|
| 198 |
+
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);}
|
| 199 |
+
const palettes={ground:["#9b8152","#a0ac6b"],pipe:["#507e40","#83a352"],question:["#d2ad4d","#f0cf66"],brick:["#aa704e","#d29463"],used:["#947d58","#baa377"],platform:["#6f8258","#a7b075"]};
|
| 200 |
+
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");}});
|
| 201 |
+
(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);});
|
| 202 |
+
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);}
|
| 203 |
+
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);}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
return {render};
|
| 208 |
+
}
|
| 209 |
+
global.ArenaRenderer={create};
|
| 210 |
+
})(window);
|