creatures/creatures.test.js
76 lines · in natura-src-1.0.0.zip as natura/creatures/creatures.test.js · a test
// creatures/creatures.test.js -- smoke test for the creature factory (node).
// Covers the parts that do not need a canvas: data integrity (species.js), behaviour loops
// (behaviour.js), and facade composition (creatures.js). The drawing in render.js is verified
// visually via ?montage= / ?zoom= renders; this guards the data + wiring that feed it.
// run: node creatures/creatures.test.js
'use strict';
const assert = require('assert');
const Species = require('./species');
const Behaviour = require('./behaviour');
const Creatures = require('./creatures');
let n = 0; const ok = (c, m) => { assert.ok(c, m); n++; };
// ---- species data: the expected roster + required fields on every entry -----------------------
const QUAD = ['deer','wolf','hare','fox','fawn','boar','bear','cougar','piglet','badger','hedgehog','mouse','squirrel'];
const BIRD = ['robin','heron','crow','duck','owl','barnowl'];
for (const k of QUAD) { const s = Species.SPECIES[k]; ok(s, 'species ' + k + ' exists');
for (const f of ['L','H','leg','neck','head','gait','col','belly']) ok(s[f] != null, k + ' has ' + f);
ok(Array.isArray(s.col) && s.col.length === 3, k + ' col is a 3-tone ramp');
ok(!Species.isBird(k), k + ' is not a bird'); }
for (const k of BIRD) { const b = Species.BIRDS[k]; ok(b, 'bird ' + k + ' exists');
for (const f of ['bodyL','bodyH','neck','beak','wing','leg','gait','col']) ok(b[f] != null, k + ' has ' + f);
ok(Species.isBird(k), k + ' is a bird'); }
const SNAKE = ['adder','grass','corn']; // the third body plan: limbless
for (const k of SNAKE) { const s = Species.SNAKES[k]; ok(s, 'snake ' + k + ' exists');
for (const f of ['L','girth','waves','amp','head','col','belly','pupil']) ok(s[f] != null, k + ' has ' + f);
ok(Array.isArray(s.col) && s.col.length === 3, k + ' col is a 3-tone ramp');
ok(Species.isSnake(k) && !Species.isBird(k), k + ' is a snake, not a bird'); }
// the OWL extends the bird plan with four new features -- assert the model widened, not reskinned
const owl = Species.BIRDS.owl;
ok(owl.facialDisc, 'owl has a facial disc');
ok(owl.forwardEyes === true, 'owl has forward-facing eyes');
ok(owl.beakType === 'hook', 'owl has a hooked beak');
ok(owl.earTufts === true, 'owl has ear tufts');
ok(owl.barred, 'owl has barred underparts');
ok(Behaviour.BEH.roost && Behaviour.BEH.roost.seg.every(s => s.stand === 1 && s.adv === 0), 'roost keeps the owl planted (never walks)');
// the BARN OWL reuses the owl machinery as a different owl -- a heart disc + dark eyes + no tufts
const barn = Species.BIRDS.barnowl;
ok(barn.discShape === 'heart', 'barn owl has a heart-shaped disc');
ok(barn.eyeDark === true, 'barn owl has dark eyes');
ok(!barn.earTufts, 'barn owl has no ear tufts');
ok(barn.forwardEyes === true && barn.facialDisc, 'barn owl reuses forward eyes + facial disc');
// the SQUIRREL extends the quadruped plan with the arched bushy tail (a new tail carriage, not a reskin)
const squ = Species.SPECIES.squirrel;
ok(squ.archTail === true, 'squirrel has an arched tail');
ok(squ.bushy > 0, 'squirrel has a bushy (plumed) tail');
ok(squ.whiskers === true, 'squirrel has whiskers');
ok(Species.get('deer') === Species.SPECIES.deer, 'get resolves a quadruped');
ok(Species.get('heron') === Species.BIRDS.heron, 'get resolves a bird');
ok(Species.get('adder') === Species.SNAKES.adder, 'get resolves a snake');
ok(Species.get('nope') === null, 'get returns null for an unknown name');
// ---- behaviour: each loop's segment fractions sum to 1; targetOf returns a segment ------------
for (const [name, B] of Object.entries(Behaviour.BEH)) {
const sum = B.seg.reduce((a, s) => a + s.f, 0);
ok(Math.abs(sum - 1) < 1e-9, name + ' segment fractions sum to 1');
}
const seg = Behaviour.targetOf({ beh: Behaviour.BEH.graze, seed: 0 }, 3);
ok(seg && typeof seg.adv === 'number', 'targetOf returns a segment with adv');
ok(Behaviour.GALLOP.gallop === true, 'GALLOP is flagged as a gallop gait');
ok(Behaviour.TROT.trot === true, 'TROT is flagged as a trot gait');
ok(Behaviour.BEH.flee.seg.some(s => s.gait === Behaviour.GALLOP), 'flee bolts into the gallop');
// ---- facade: composition + re-exposed surface ------------------------------------------------
ok(typeof Creatures.draw === 'function', 'facade exposes draw()');
ok(Creatures.SPEC('fox') === Species.SPECIES.fox, 'facade SPEC resolves a quadruped');
ok(Creatures.isBird('crow') === true, 'facade isBird');
ok(Creatures.isSnake('adder') === true && Creatures.isSnake('deer') === false, 'facade isSnake');
ok(Creatures.BEH === Behaviour.BEH, 'facade re-exposes BEH');
ok(Creatures.TROT === Behaviour.TROT, 'facade re-exposes TROT');
ok(typeof Creatures.mulberry === 'function', 'facade exposes the seeded RNG');
const rng = Creatures.mulberry(42), v = rng();
ok(v >= 0 && v < 1, 'mulberry yields [0,1)');
ok(Creatures.mulberry(42)() === v, 'mulberry is deterministic by seed');
console.log('creatures: ' + n + ' checks passed');