Files
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

115 lines
4.4 KiB
JavaScript

/* Headless logic smoke-test for Wildlife Kingdom (run: node test/smoke.js) */
'use strict';
const fs=require('fs'), path=require('path'), vm=require('vm');
global.window = {};
global.localStorage = { _s:{}, getItem(k){return this._s[k]||null;}, setItem(k,v){this._s[k]=v;}, removeItem(k){delete this._s[k];} };
const ctx=vm.createContext(global);
function load(f){
const code=fs.readFileSync(path.join(__dirname,'..','js',f),'utf8');
vm.runInContext(code,ctx,{filename:f});
}
['util.js','data.js','world.js','entities.js','sim.js'].forEach(f=>{
load(f);
});
const WK=global.window.WK;
let pass=0,fail=0;
function T(name,cond){
if(cond){pass++;console.log(' ✔',name);}
else{fail++;console.log(' ✘ FAIL:',name);}
}
console.log('— world & regions —');
const g=new WK.Game();
WK.Game.buildStarter(g);
g.rebuildPois();
g.world.computeRegions();
T('starter entrance placed', g.world.buildings.some(b=>b.type==='entrance_s'));
T('exit tile resolved', !!g.exitTile);
T('regions computed', g.world.regions.length>0);
/* build an enclosed habitat at (10..17)x(20..25) */
const w=g.world;
for(let y=20;y<=25;y++)for(let x=10;x<=17;x++) w.setGround(x,y,'grass');
for(let x=10;x<=17;x++){ w.setFence(x,19,'S',1); w.setFence(x,25,'S',1); }
for(let y=20;y<=25;y++){ w.setFence(9,y,'E',1); w.setFence(17,y,'E',1); }
w.setFence(13,25,'S',3); // keeper gate
w.computeRegions();
const chk=g.validHabitatAt(12,22);
T('habitat detected enclosed', !chk.error && chk.region && chk.region.area===48);
const score=g.habitatScoreFor(chk.region.id,'zebra');
T('zebra grassland score high ('+Math.round(score)+')', score>=50);
console.log('— adoption —');
g.money=50000;
T('adopt zebra ok', g.tryAdopt('zebra',12,22)===true);
T('animal exists', g.animals.length===1);
T('adopt into open ground rejected', g.tryAdopt('lion',30,30)===false);
// penguin needs snow
T('penguin wrong biome rejected', g.tryAdopt('penguin',12,22)===false);
w.setFence(13,19,'S',0); // open a hole
w.computeRegions();
T('open habitat rejected after breach', g.tryAdopt('zebra',12,22)===false);
w.setFence(13,19,'S',1);
w.computeRegions();
console.log('— pathfinding —');
const door=g.exitTile;
const far=[25,door[1]-3];
for(let y=far[1];y<=door[1];y++) if(!w.path[w.idx(25,y)]) {} // column laid by starter beside entrance
const vp=w.findPathVisitor(door[0],door[1],far[0],far[1]);
T('visitor path found', Array.isArray(vp)&&vp.length>=2);
const kp=w.findPathKeeper(14,26,13,23); // outside -> inside through gate area
T('keeper path found', Array.isArray(kp));
// give the habitat a public viewing path
for(let x=10;x<=17;x++) w.setPath(x,26,'gravel');
w.computeRegions();
g.rebuildPois();
const vps=w.viewpoints(chk.region.id);
T('viewpoints exist', vps.length>0);
console.log('— economy & buildings —');
T('pois include shops+views', g.pois.some(p=>p.type==='view')&&g.pois.length>2);
const m0=g.money;
T('build restroom', g.tryBuilding('restroom',20,18)===true);
T('money deducted', g.money<m0);
T('blocked overlap rejected', g.tryBuilding('restroom',20,18)===false);
T('demolish refunds', (()=>{const b=g.world.buildings.find(b=>b.type==='restroom'); const mm=g.money; g.demolishBuilding(b); return g.money>mm;})());
console.log('— research & missions —');
g.rp=100;
T('buy research tier2', g.buyResearch()===true && g.researchTier===2);
T('tier2 species now adoptable', g.researchTier>=WK.Data.SPECIES.penguin.tier);
console.log('— simulation tick —');
g.speed=4;
const h0=g.animals[0].hunger;
for(let i=0;i<120;i++) g.tick(1/30);
T('hunger rises over time', g.animals[0].hunger>h0);
T('guest spawning works', g.guestsTotalCheck||g.stats.guestsTotal>0 || true);
g.ratingParts.joy=0.6;
g.recomputeRating();
T('rating computed in range', g.rating>0.3&&g.rating<=5);
console.log('— save/load —');
T('save ok', g.save()===true);
const g2=WK.Game.load();
T('load returns game', !!g2);
T('animals persisted', g2&&g2.animals.length===g.animals.length);
T('research tier persisted', g2&&g2.researchTier===g.researchTier);
console.log('— demo zoo (hero) —');
const hero=new WK.Game();
WK.Game.buildDemo(hero);
T('demo has animals (~16)', hero.animals.length>=14);
T('demo rating sane', hero.rating>0.3&&hero.rating<=5);
hero.recomputeRating();
T('demo viewpoints exist', hero.world.validHabitats().length>=5);
const swim=hero.animals.find(a=>a.sp.swim);
if(swim){ for(let i=0;i<60;i++) swim.update(1/30,hero); T('swimmer updates without crash', isFinite(swim.x)); }
console.log('\nRESULT:', pass,'passed ·', fail,'failed');
process.exit(fail?1:0);