Files
wildlife-kingdom/test/qa.html
T
deepseek c06786518c Wildlife Kingdom — original browser zoo-management sim
- Procedural canvas art: 15 species × 4 poses, 15 buildings, modular terrain, UI icons
- Habitat enclosure detection, biome suitability, staff/guest AI pathfinding
- Economy: tickets, shops, wages, star rating, research tiers, 15 missions
- Day/night cycle, particles, WebAudio soundtrack & synth SFX
- Mouse + touch controls, responsive UI, localStorage autosave
- Verified: 31/31 logic tests (test/smoke.js) · 12/12 render audits (test/qa.html)
2026-08-23 07:01:21 +00:00

196 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html><head><meta charset="utf8"><title>QARUN</title></head><body>
<canvas id="game" width="1200" height="800"></canvas>
<pre id="out">PENDING</pre>
<script src="../js/util.js"></script>
<script src="../js/data.js"></script>
<script src="../js/audio.js"></script>
<script src="../js/sprites.js"></script>
<script src="../js/world.js"></script>
<script src="../js/entities.js"></script>
<script src="../js/sim.js"></script>
<script src="../js/render.js"></script>
<script>
'use strict';
window.onerror=(m,s,l)=>{ document.getElementById('out').textContent='JSERROR: '+m+' @'+s+':'+l; };
(async function(){
const out={checks:[],errors:[]};
const T=(name,cond,info)=>out.checks.push((cond?'PASS ':'FAIL ')+name+(info?(' :: '+info):''));
try{
const WK=window.WK;
const canvas=document.getElementById('game');
const renderer=new WK.Renderer(canvas);
// force css size (headless has no layout box matching)
renderer.cssW=1200; renderer.cssH=800;
renderer.canvas.width=1200; renderer.canvas.height=800;
renderer.canvas.style.width='1200px'; renderer.canvas.style.height='800px';
const cam=new WK.Camera();
cam.resize(1200,800);
/* ---------- hero demo zoo render ---------- */
const hero=new WK.Game();
WK.Game.buildDemo(hero);
hero.timeAbs=1.2;
cam.centerOnTile(hero.world.cols-2,hero.world.rows-3);
cam.zoom=0.95;
renderer.frame(hero,cam,{});
await new Promise(r=>setTimeout(r,50));
let img=renderer.ctx.getImageData(0,0,1200,800).data;
const colors=new Set();
for(let i=0;i<img.length;i+=40){ colors.add((img[i]>>3)+','+(img[i+1]>>3)+','+(img[i+2]>>3)); }
T('demo frame rich color count',colors.size>400,String(colors.size));
// sky should be blue-ish at top (camera at south corner → upper screen is off-map)
const px=(x,y)=>{ const k=(y*1200+x)*4; return [img[k],img[k+1],img[k+2]]; };
const sky=px(600,8);
T('sky blue at top',sky[2]>140&&sky[2]>sky[0],sky.join(','));
// center should contain green grass tones somewhere in middle band
let greens=0,total=0;
for(let y=300;y<500;y+=10)for(let x=100;x<1100;x+=10){
const c=px(x,y); total++;
if(c[1]>c[0]&&c[1]>c[2]&&c[1]>90) greens++;
}
T('green terrain visible mid-frame',greens/total>0.25,Math.round(greens/total*100)+'%');
// water shimmer: some cyan-blue pixels
let blues=0;
for(let y=250;y<700;y+=7)for(let x=150;x<1050;x+=7){
const c=px(x,y);
if(c[2]>130&&c[2]>c[0]+30&&c[1]>110) blues++;
}
T('water tones present',blues>12,String(blues));
/* ---------- night frame ---------- */
hero.minutes=23*60;
renderer.frame(hero,cam,{});
await new Promise(r=>setTimeout(r,20));
img=renderer.ctx.getImageData(0,0,1200,800).data;
const night=px(600,8);
T('night darkens sky',night[2]<sky[2],night.join(','));
/* ---------- starter zoo + entities draw ---------- */
const g=new WK.Game();
WK.Game.buildStarter(g);
g.rebuildPois();
// add animal & guest & keeper directly into view
g.world.computeRegions();
const rid=g.world.regionOf(23,36);
if(rid>0){}else{ /* no habitat in starter; force one */ }
g.money=99999;
// build quick habitat near starter (interior 18..24 × 28..33)
const w=g.world;
for(let y=28;y<=33;y++)for(let x=18;x<=24;x++) w.setGround(x,y,'grass');
for(let x=17;x<=24;x++){ w.setFence(x,27,'S',1); w.setFence(x,33,'S',1); }
for(let y=28;y<=33;y++){ w.setFence(17,y,'E',1); w.setFence(24,y,'E',1); }
w.computeRegions();
const reg=w.validHabitats()[0];
T('qa habitat valid',!!reg,String(reg&&reg.area));
if(reg){
g.tryAdopt('zebra',21,30);
g.tryAdopt('flamingo',21,31);
}
g.guests.push(new WK.Guest(g,23,41));
g.staff.push(new WK.Staff('keeper',22,41));
g.animals[0]&&(g.animals[0].state='happy');
cam.centerOnTile(22,34);
cam.zoom=1.15;
hero.minutes=12*60;
g.timeAbs=2.5; g.minutes=12*60;
renderer.frame(g,cam,{grid:true});
await new Promise(r=>setTimeout(r,50));
img=renderer.ctx.getImageData(0,0,1200,800).data;
const px2=(x,y)=>{ const k=(y*1200+x)*4; return [img[k],img[k+1],img[k+2]]; };
// HUD-free center: expect path beige tones near plaza
let beiges=0;
for(let y=350;y<650;y+=6)for(let x=200;x<1000;x+=6){
const c=px2(x,y);
if(c[0]>190&&c[1]>170&&c[2]>130&&c[0]>=c[1]&&c[1]>c[2]) beiges++;
}
T('starter paths visible',beiges>40,String(beiges));
/* ---------- sprite icon audit ---------- */
let blankIcons=[];
const kinds=['res:coin','res:guest','res:heart','res:flask','res:star','ground:grass','ground:snow','path:gravel','path:stonep','fence:wood','fence:gate','nature:tree','nature:palm','nature:lamp','animal:lion','animal:panda','animal:penguin','animal:elephant','facility:aquarium','facility:hut','facility:entrance_g'];
for(const kd of kinds){
const s=WK.Sprites.getIcon(kd,52);
const d=s.cv.getContext('2d').getImageData(0,0,s.cv.width,s.cv.height).data;
let a=0; for(let i=3;i<d.length;i+=16) a+=d[i];
if(a<40) blankIcons.push(kd);
}
T('all UI icons painted',blankIcons.length===0,blankIcons.join(',')||'ok');
/* ---------- all species draw without error ---------- */
let badSpecies=[];
for(const sp of Object.keys(WK.Data.SPECIES)){
const cv=WK.mkCanvas(160,160); const cx=cv.getContext('2d');
cx.translate(80,140);
try{
for(const st of ['idle','walk','eat','happy'])
WK.Sprites.drawAnimal(cx,sp,{state:st,ph:1.3,t:1.7});
const d=cv.getContext('2d').getImageData(0,0,160,160).data;
let a=0; for(let i=3;i<d.length;i+=64) a+=d[i];
if(a<25) badSpecies.push(sp+':dim');
}catch(e){ badSpecies.push(sp+':'+e.message); }
}
T('15 species × 4 poses render',badSpecies.length===0,badSpecies.join(',')||'ok');
/* ---------- buildings render ---------- */
let badBld=[];
for(const b of Object.keys(WK.Data.BUILDING)){
try{
const s=WK.Sprites.getBuilding(b);
const d=s.cv.getContext('2d').getImageData(0,0,s.cv.width,s.cv.height).data;
let a=0; for(let i=3;i<d.length;i+=97) a+=d[i];
if(a<30) badBld.push(b+':dim');
}catch(e){ badBld.push(b+':'+e.message); }
}
T('15 buildings render',badBld.length===0,badBld.join(',')||'ok');
/* ---------- fence continuity (regression: corner-exact endpoints) ---------- */
/* Wall topology in this iso model:
· S-edges chain over +x → "\" wall · E-edges chain over +y → "/" wall */
{
const w2=new WK.World(20,20);
w2.setFence(5,5,'E',1); w2.setFence(5,6,'E',1); w2.setFence(5,7,'E',1); // "/" run over +y
const cam2=new WK.Camera(); cam2.resize(420,320);
cam2.centerOnTile(5.4,6.4); cam2.zoom=1.5;
const cv2=WK.mkCanvas(420,320), c2=cv2.getContext('2d');
const z=cam2.zoom;
// draw exactly like render.js does
for(const ty of [5,6,7]){
const pp=cam2.project(5,ty);
WK.Sprites.drawFenceEdge(c2,pp.x,pp.y+WK.HH*z,pp.x+WK.HW*z,pp.y,1,0);
}
// sample along the rail from E-corner of (5,5) to S-corner of (5,7)
const t55=cam2.project(5,5), t57=cam2.project(5,7);
const sx=t55.x+WK.HW*z, sy=t55.y; // start (E corner, top)
const ex=t57.x, ey=t57.y+WK.HH*z; // end (S corner, bottom)
let gaps=0,samples=0;
for(let tt=0.02;tt<=0.98;tt+=0.01){
samples++;
const xx=Math.round(sx+(ex-sx)*tt), yy=Math.round(sy+(ey-sy)*tt-5); // mid rail
const d=c2.getImageData(xx,yy,1,1).data;
if(d[3]<10) gaps++;
}
T('straight fence run continuous',gaps<=1,gaps+'/'+samples+' gaps');
// L-corner joint: E-edge of (5,5) meets S-edge of (5,5) at the shared S corner
const cv3=WK.mkCanvas(420,320), c3=cv3.getContext('2d');
const p55=cam2.project(5,5);
WK.Sprites.drawFenceEdge(c3,p55.x,p55.y+WK.HH*z,p55.x+WK.HW*z,p55.y,1,0);
WK.Sprites.drawFenceEdge(c3,p55.x-WK.HW*z,p55.y,p55.x,p55.y+WK.HH*z,1,0);
const jx=Math.round(p55.x), jy=Math.round(p55.y+WK.HH*z-8); // post zone at joint
let post=0;
for(let dx=-2;dx<=2;dx++)for(let dy=-2;dy<=2;dy++){
const d=c3.getImageData(jx+dx,jy+dy,1,1).data;
if(d[3]>60&&d[0]>90&&d[0]<200&&d[1]>60&&d[2]<130) post++;
}
T('corner joint has shared post',post>=8,String(post));
}
}catch(e){
out.errors.push(e.stack||String(e));
}
document.getElementById('out').textContent='QAJSON '+JSON.stringify(out);
document.title='QADONE';
})();
</script>
</body></html>