← Natura·Docs/Modules/Source

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 };
});