← Natura·Docs/Modules/Source

canopy-test.js

76 lines · in natura-src-1.0.0.zip as natura/canopy-test.js · a test

// canopy-test.js -- assertions for the canopy light-field system (render-free structure the organisms + renderer read).
const N = require('./natura.js');
const { makeCanopy } = require('./canopy.js');
const { rngFrom, makeClock, UNITS, World } = N;
const DAY = UNITS.DAY;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
const atNoon = f => f;   // readability
console.log('canopy assertions:');

// build one canopy with a clock we can scrub
function make(seed, cfg) { const sys = makeCanopy(cfg || {}); const clock = makeClock(0); const st = sys.init(rngFrom(seed), sys.params, clock); return { sys, st, clock }; }
function fieldsAt(c, dayPhase) { c.clock.t = dayPhase * DAY; return c.sys.fields(c.st); }

// 1. GAPS: count in range, positions in [0.05,0.95], deterministic by seed
{ const a = make(7), b = make(7); const F = a.sys.fields(a.st), G = b.sys.fields(b.st);
  ok(F.gaps.length >= 4 && F.gaps.length <= 6, 'a handful of sky-gaps (' + F.gaps.length + ')');
  ok(F.gaps.every(x => x >= 0.05 && x <= 0.95), 'gaps sit within the frame');
  ok(F.gaps.length === G.gaps.length && F.gaps.every((x, i) => x === G.gaps[i]), 'same seed -> identical gaps (deterministic)');
}

// 2. TRANSMISSION falls with depth: ~1 at the top, a sliver at the floor
{ const F = make(7).sys.fields(make(7).st);
  ok(Math.abs(F.transmission(0.02) - 1) < 0.02, 'full light above the canopy');
  ok(F.transmission(0.9) < 0.12, 'only a sliver reaches the floor (' + F.transmission(0.9).toFixed(3) + ')');
  ok(F.transmission(0.2) > F.transmission(0.5) && F.transmission(0.5) > F.transmission(0.9), 'transmission is monotone down the column');
}

// 3. SHADE rises with depth and is capped (so noon still reads as day)
{ const F = make(7).sys.fields(make(7).st);
  ok(F.shadeAt(0.2) < F.shadeAt(0.9), 'the floor carries more light-debt than the canopy');
  ok(F.shadeAt(0.99) <= 0.6 + 1e-9, 'shade is capped at 0.6 (a nocturnal thing still sleeps at noon)');
}

// 4. lightAt at NOON: bright canopy, dim floor
{ const c = make(7); const F = fieldsAt(c, 0.5);
  ok(F.lightAt(0.2) > 0.3 && F.lightAt(0.9) < 0.12, 'noon: canopy bright (' + F.lightAt(0.2).toFixed(2) + '), floor dim (' + F.lightAt(0.9).toFixed(2) + ')');
  ok(F.lightAt(0.2) > F.lightAt(0.9) * 3, 'the floor gets a small fraction of the canopy light');
}

// 5. sunAt: signed sun, day positive / night negative; floor dimmer than canopy at noon
{ const c = make(7);
  const noon = fieldsAt(c, 0.5); ok(noon.sunAt(0.5, 0.05) > 0.9 && noon.sunAt(0.5, 0.9) > 0.1 && noon.sunAt(0.5, 0.9) < noon.sunAt(0.5, 0.2), 'noon: emergent sun ~1, floor sun lower but still day (canopy band carries modest shade)');
  const night = fieldsAt(c, 0.0); ok(night.sunAt(0.5, 0.2) < -0.5 && night.sunAt(0.5, 0.9) < -0.5, 'midnight: sun negative everywhere');
}

// 6. REGION: at dawn the east (+x) is further into day than the west (-x)
{ const c = make(7); const dawn = fieldsAt(c, 0.25);
  ok(dawn.sunAt(0.9, 0.3) > dawn.sunAt(0.1, 0.3), 'dawn sweeps across: east ahead of west (' + dawn.sunAt(0.9, 0.3).toFixed(2) + ' vs ' + dawn.sunAt(0.1, 0.3).toFixed(2) + ')');
}

// 7. the FLOOR keeps the SHORTEST day: over a full day, fewer sunlit samples on the floor than the canopy
{ const c = make(7); let floorDay = 0, canopyDay = 0, S = 240;
  for (let i = 0; i < S; i++) { const F = fieldsAt(c, i / S); if (F.sunAt(0.5, 0.9) > 0) floorDay++; if (F.sunAt(0.5, 0.2) > 0) canopyDay++; }
  ok(floorDay < canopyDay, 'the floor is sunlit for fewer of the day-samples than the canopy (' + floorDay + ' vs ' + canopyDay + ')');
}

// 8. COMPOSES in a World and exposes its field functions
{ const sys = makeCanopy({}); const w = World({ seed: 3, systems: [sys] }); w.step(UNITS.HOUR);
  const F = w.fields().canopy; ok(F && typeof F.sunAt === 'function' && typeof F.lightAt === 'function' && Array.isArray(F.gaps), 'canopy composes in a Natura World and exposes sunAt/lightAt/gaps');
}

// 9. CLOUD coupling: an overcast sky (read from a weather field) dims the daylight + lightAt, not the sun's timing
{ const c = make(7); c.clock.t = 0.5 * DAY;
  const C0 = c.sys.fields(c.st); const clear = C0.daylight, clearLight = C0.lightAt(0.2), clearSun = C0.sunAt(0.5, 0.2);
  c.sys.step(c.st, { fields: { weather: { cloud: 0.9 } } }, 1);            // heavy cloud rolls in
  const F = c.sys.fields(c.st);
  ok(F.daylight < clear * 0.6, 'cloud dims the canopy daylight (' + clear.toFixed(2) + ' clear -> ' + F.daylight.toFixed(2) + ' overcast)');
  ok(F.lightAt(0.2) < clearLight * 0.6, 'cloud dims the light reaching the canopy (so the shafts + morphos fade)');
  ok(F.cloud === 0.9, 'canopy exposes the cloud cover it read from the weather field');
  ok(Math.abs(F.sunAt(0.5, 0.2) - clearSun) < 1e-9, 'the sun POSITION (day/night timing) is unchanged by cloud');
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);