natura.js
119 lines · in natura-src-1.0.0.zip as natura/natura.js · a module · its entry in the module reference · its test: natura-test.js
// natura.js -- the CORE of the Natura pocket-universe framework (see DESIGN.md, ONTOLOGY.md).
// The beating heart everything plugs into: a universal CLOCK (one time, every cadence day->eon), a
// common SYSTEM interface (generalizing the Terrain Engine's PROCESS -- init/step/fields/events,
// deterministic + testable), a REGISTRY of system definitions, and the WORLD runner that steps a chosen
// STACK of systems and COUPLES them (each system reads the others' exposed fields). Composable: a world
// is a chosen set of systems; run one (frost on a leaf) or many (planet+climate+weather+life).
//
// Deterministic by construction: the clock time and dt are INPUTS, randomness comes from a seeded rng
// (no Date.now / Math.random) -- so a run is reproducible and resumable. UMD (node test / window.Natura).
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Natura = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
// --- time units (seconds), spanning the operator's day->eon ladder --------------------------------
const U = {};
U.SECOND = 1; U.MINUTE = 60; U.HOUR = 3600; U.DAY = 86400; U.WEEK = 7 * U.DAY;
U.MONTH = 30 * U.DAY; U.YEAR = 365 * U.DAY; U.DECADE = 10 * U.YEAR; U.CENTURY = 100 * U.YEAR;
U.MILLENNIUM = 1000 * U.YEAR; U.EON = 1e9 * U.YEAR;
// --- deterministic rng (mulberry32); derive independent streams from a seed ------------------------
function rngFrom(seed) {
let a = seed >>> 0;
return function () {
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
const mix = (a, b) => (((a >>> 0) * 2654435761) ^ ((b >>> 0) * 40503)) >>> 0;
// --- the CLOCK: one time, phase helpers at every cadence ------------------------------------------
// phase(period) in [0,1) is the fraction through that cycle. day/season/etc. derive from it, so
// "day night week month year decade century" cycles are all one clock read at different cadences.
function makeClock(t0) {
let t = t0 || 0;
const phase = (period) => { const m = ((t % period) + period) % period; return m / period; };
return {
get t() { return t; },
set t(v) { t = v; },
advance(dt) { t += dt; return t; },
phase,
units: U,
dayPhase() { return phase(U.DAY); }, // 0 = midnight, 0.5 = noon
yearPhase() { return phase(U.YEAR); }, // 0 = year start (spring)
daylight() { return Math.max(0, -Math.cos(phase(U.DAY) * 2 * Math.PI)); }, // 0 night .. 1 noon
season() { return ['spring', 'summer', 'autumn', 'winter'][Math.floor(phase(U.YEAR) * 4) % 4]; },
seasonPhase() { return phase(U.YEAR); }, // continuous 0..1 through the year
years() { return t / U.YEAR; }, // absolute years (for deep-time systems)
eons() { return t / U.EON; }
};
}
// --- the SYSTEM registry. A system definition is a plain object (see DESIGN.md) ---------------------
// { name, domain, scale, params, init(rng, params, clock)->state,
// step(state, ctx, dt)->state, fields(state)->obj, events(state)->[] }
const _registry = new Map();
function defineSystem(spec) {
if (!spec || !spec.name) throw new Error('a system needs a name');
_registry.set(spec.name, spec);
return spec;
}
function getSystem(name) { return _registry.get(name); }
function clearRegistry() { _registry.clear(); } // for tests
// --- the WORLD: a chosen stack of systems, stepped and coupled -------------------------------------
// spec: { seed, t0, systems: [ 'name' | {name,params} | <definition> ] }
function World(spec) {
spec = spec || {};
const seed = (spec.seed == null ? 1 : spec.seed) >>> 0;
const clock = makeClock(spec.t0 || 0);
const worldRng = rngFrom(mix(seed, 991));
const chosen = (spec.systems || []).map((s, i) => {
let def, params;
if (typeof s === 'string') { def = getSystem(s); params = def && def.params ? def.params : {}; }
else if (s && s.step != null || (s && s.init != null)) { def = s; params = s.params || {}; } // a definition passed inline
else if (s && s.name) { def = getSystem(s.name); params = Object.assign({}, def && def.params, s.params); }
if (!def) throw new Error('unknown system: ' + (typeof s === 'string' ? s : (s && s.name)));
const sysRng = rngFrom(mix(seed, (i + 1) * 7919));
const state = def.init ? def.init(sysRng, params, clock) : {};
return { def, state, params, name: def.name };
});
const byName = {}; chosen.forEach(c => { byName[c.name] = c; });
// the coupling snapshot: every system's exposed fields, keyed by system name
function fields() {
const f = {};
for (const c of chosen) if (c.def.fields) f[c.name] = c.def.fields(c.state);
return f;
}
let lastEvents = [];
function step(dt) {
clock.advance(dt);
const snap = fields(); // pre-step fields -> order-independent coupling
const ctx = { clock, rng: worldRng, fields: snap, world: byName, dt };
const evs = [];
for (const c of chosen) {
if (c.def.step) { const ns = c.def.step(c.state, ctx, dt); if (ns !== undefined) c.state = ns; }
if (c.def.events) { const e = c.def.events(c.state); if (e && e.length) for (const x of e) evs.push(Object.assign({ system: c.name, t: clock.t }, x)); }
}
lastEvents = evs;
return { t: clock.t, events: evs };
}
// run n steps of dt (convenience); returns all events
function run(steps, dt) { const all = []; for (let i = 0; i < steps; i++) all.push(...step(dt).events); return all; }
return {
clock, seed, step, run, fields,
get(name) { return byName[name] ? byName[name].state : undefined; },
has(name) { return !!byName[name]; },
systems: chosen.map(c => c.name),
events() { return lastEvents; }
};
}
return { World, makeClock, defineSystem, getSystem, clearRegistry, rngFrom, mix, UNITS: U };
});