- 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)
115 lines
4.4 KiB
JavaScript
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);
|