2026-09-29 19:10:03 +09:00
|
|
|
import assert from 'node:assert/strict';
|
|
|
|
|
import {readFile} from 'node:fs/promises';
|
2026-09-29 23:45:12 +09:00
|
|
|
import {World,DT} from '../engine.js?v=45';
|
|
|
|
|
import {fieldResourceIntakeKgPerDay,interactionEdge} from '../ecology/feeding.js?v=45';
|
2026-09-29 19:10:03 +09:00
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
const quantile=(a,p)=>a.sort((x,y)=>x-y)[Math.floor((a.length-1)*p)]||0;
|
2026-09-29 19:10:03 +09:00
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
// 1) Module contract: catches missing named exports before browser release.
|
|
|
|
|
assert.equal(typeof fieldResourceIntakeKgPerDay,'function');
|
|
|
|
|
const edge=interactionEdge('primary-consumer','producer');
|
|
|
|
|
assert.ok(fieldResourceIntakeKgPerDay(8,.3,edge)>fieldResourceIntakeKgPerDay(8,.02,edge));
|
2026-09-29 19:10:03 +09:00
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
// 2) User water edits must remain exact.
|
2026-09-29 19:10:03 +09:00
|
|
|
{
|
2026-09-29 23:45:12 +09:00
|
|
|
const w=new World(77), edited=w.water.map((v,i)=>({...v,r:i===0?v.r*.98:v.r}));
|
|
|
|
|
assert.equal(w.setWater(edited),true);
|
|
|
|
|
assert.deepEqual(w.water,edited);
|
2026-09-29 19:10:03 +09:00
|
|
|
}
|
|
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
// 3) Free-movement smoke test: no spawn lock, widespread freezing, or ping-pong reversal.
|
|
|
|
|
const w=new World(481516), start=new Map(w.animals.map(a=>[a.id,{x:a.x,y:a.y}]));
|
|
|
|
|
const track=new Map(w.animals.map(a=>[a.id,{x:a.x,y:a.y,still:0,ticks:0,dx:0,dy:0,moves:0,rev:0}]));
|
|
|
|
|
for(let step=0;step<3*24;step++){
|
|
|
|
|
w.step();
|
|
|
|
|
for(const a of w.animals){
|
|
|
|
|
if(a.dead||!track.has(a.id))continue;
|
|
|
|
|
const t=track.get(a.id),dx=a.x-t.x,dy=a.y-t.y,d=Math.hypot(dx,dy),last=Math.hypot(t.dx,t.dy);
|
|
|
|
|
t.ticks++; if(d<1e-6)t.still++;
|
|
|
|
|
if(d>.1){t.moves++;if(last>.1&&(dx*t.dx+dy*t.dy)/(d*last)<-.7)t.rev++;t.dx=dx;t.dy=dy}
|
|
|
|
|
t.x=a.x;t.y=a.y;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const alive=w.animals.filter(a=>!a.dead&&start.has(a.id));
|
|
|
|
|
const displacement=alive.map(a=>Math.hypot(a.x-start.get(a.id).x,a.y-start.get(a.id).y));
|
|
|
|
|
const still=alive.map(a=>track.get(a.id).still/track.get(a.id).ticks);
|
|
|
|
|
const reversal=alive.map(a=>track.get(a.id).rev/Math.max(1,track.get(a.id).moves));
|
|
|
|
|
assert.ok(quantile(displacement,.1)>100);
|
|
|
|
|
assert.ok(quantile(displacement,.5)>350);
|
|
|
|
|
assert.ok(quantile(still,.9)<.3);
|
|
|
|
|
assert.ok(quantile(reversal,.9)<.22);
|
|
|
|
|
|
|
|
|
|
// 4) Short population smoke: births and at least one net-increase day must be possible.
|
|
|
|
|
const pop=new World(481516), firstNextId=pop.nextId;let previous=pop.stats().population,sawIncrease=false;
|
|
|
|
|
for(let day=0;day<14;day++){
|
|
|
|
|
for(let i=0;i<24;i++)pop.step();
|
|
|
|
|
const now=pop.stats().population;if(now>previous)sawIncrease=true;previous=now;
|
|
|
|
|
}
|
|
|
|
|
assert.ok(pop.nextId>firstNextId);
|
|
|
|
|
assert.ok(sawIncrease);
|
|
|
|
|
assert.ok(pop.animals.every(a=>[a.x,a.y,a.energy,a.age,a.structuralMassKg].every(Number.isFinite)));
|
2026-09-29 19:10:03 +09:00
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
// 5) Removed constraints/workarounds must not return.
|
|
|
|
|
const productionFiles=['engine.js','worker.js','ecology/movement.js'];
|
|
|
|
|
const code=(await Promise.all(productionFiles.map(f=>readFile(new URL('../'+f,import.meta.url),'utf8')))).join('\n');
|
|
|
|
|
assert.ok(!/homeRange|homeX|homeY|counter-torque|rotationIndex|angularScale/i.test(code));
|
|
|
|
|
assert.ok(!/set-interactions|set-climate|run-batch|schedule-disturbance/i.test(code));
|
2026-09-29 19:10:03 +09:00
|
|
|
|
2026-09-29 23:45:12 +09:00
|
|
|
console.log(JSON.stringify({ok:true,dtDays:DT,animals:pop.stats().population,births:pop.nextId-firstNextId},null,2));
|