← Natura·Docs/Modules/Source

verbs/court.js

96 lines · in natura-src-1.0.0.zip as natura/verbs/court.js · a module · its entry in the module reference · its test: verbs/court-test.js

// verbs/court.js -- ONE life ontology verb, one concern: COURTSHIP + MATE CHOICE, the engine of SEXUAL selection
// (distinct from natural selection: who SURVIVES vs who is CHOSEN). Pure and RENDER-FREE, the small-file sibling of
// daynight / forage / flee / phenology. Universal to any sexually-reproducing life: a peacock's train, a bowerbird's
// bower, a cricket's song, a stickleback's red belly, a firefly's flash-rate -- all a costly DISPLAY assessed by a
// CHOOSER whose PREFERENCE decides who breeds. The four primitives compose into the whole lifecycle the ontology
// names (display -> assess -> pair-choice -> reproduction), and -- run over generations with the heredity primitive
// below -- they produce DIRECTIONAL SEXUAL SELECTION: choosy choosers mate with the showiest displayers, so a costly
// display ESCALATES generation on generation far past anything survival would favour -- a trait grown because it is
// CHOSEN, not because it helps -- checked only by the survival COST of the display (the handicap): the peacock's train
// is as big as survival will bear and no bigger, and it does not grow at all without the choice (all three RUN-verified
// in court-test.js). The SAME verb, given a MATCHING (recognition) taste instead of an open-ended one, produces the
// OPPOSITE pole -- STABILISING selection: the display converges to the preferred VALUE and holds there, tracking it if
// it shifts, never running away (also RUN-verified) -- so the one primitive gives BOTH fundamental kinds of mate
// choice. The further LANDE-KIRKPATRICK runaway, where the PREFERENCE itself co-escalates with the display
// through their genetic covariance, is a delicate two-locus regime a dedicated life system can pursue on these
// primitives -- the covariance does not build from mean-field choice alone, recorded honestly rather than claimed.
// Nothing in the framework ran mate choice or heredity before this -- court.js is the FIRST, a new primitive. (The
// Bower is a single-bird real-time courtship INTERACTION, a different scale, not a population model this captures.)
// UMD (node / window.NaturaVerbs.court).
(function (root, factory) {
  if (typeof module !== 'undefined' && module.exports) module.exports = factory();
  else (root.NaturaVerbs = root.NaturaVerbs || {}).court = factory();
})(typeof self !== 'undefined' ? self : this, function () {
  'use strict';
  const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
  // a tiny gaussian from a uniform rng (Box-Muller half); deterministic when the rng is seeded
  function gauss(rng) { const u = Math.max(1e-9, rng()), v = rng(); return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v); }

  // attractiveness(display, preference, opts) -> how appealing THIS display is to a chooser holding THIS preference.
  //   mode 'directional' (default): bigger is better, scaled by the chooser's preference strength -> the open-ended
  //     taste that FEEDS runaway (a stronger preference values each extra unit of display more).  a = preference * display.
  //   mode 'matching': the chooser prefers a display NEAR its preference VALUE (a tuned/species-recognition taste,
  //     a Gaussian on the gap) -> stabilising, no runaway. opts.width sets the tolerance.
  function attractiveness(display, preference, opts) {
    opts = opts || {};
    if ((opts.mode || 'directional') === 'matching') {
      const w = opts.width != null ? opts.width : 0.5, d = display - preference;
      return Math.exp(-(d * d) / (2 * w * w));
    }
    return preference * display;   // directional: value grows with the display, weighted by the taste for it
  }

  // displayCost(display, opts) -> the SURVIVAL cost of carrying/making a display (the HANDICAP that keeps the signal
  //   honest and checks the runaway). Monotone increasing and usually convex (a longer tail costs disproportionately):
  //   cost = k * |display|^power. Returned as a non-negative penalty a life system subtracts from survival/energy.
  function displayCost(display, opts) {
    opts = opts || {};
    const k = opts.k != null ? opts.k : 0.02, power = opts.power != null ? opts.power : 2;
    return k * Math.pow(Math.abs(display), power);
  }

  // choose(preference, candidates, opts) -> the INDEX of the chosen mate (or -1 if none). A chooser with `preference`
  //   samples `candidates` (each {display} or a bare number) and picks by a logit/softmax on attractiveness, so choice
  //   is probabilistic but biased to the showiest; `choosiness` sharpens it (0 = random mating -> NO sexual selection,
  //   high = always the top display). `sample` caps how many candidates are assessed (a chooser rarely sees them all).
  function choose(preference, candidates, opts) {
    opts = opts || {};
    const rng = opts.rng || Math.random;
    const choosiness = opts.choosiness != null ? opts.choosiness : 3;
    const mode = opts.mode || 'directional';
    const width = opts.width;
    const n = candidates.length; if (!n) return -1;
    // optionally assess only a random subset (a real chooser samples, not censuses)
    let idx = null;
    if (opts.sample && opts.sample < n) { idx = []; for (let s = 0; s < opts.sample; s++) idx.push((rng() * n) | 0); }
    const pool = idx || null;
    const m = pool ? pool.length : n;
    const disp = (i) => { const c = candidates[pool ? pool[i] : i]; return typeof c === 'number' ? c : c.display; };
    // softmax over choosiness * attractiveness
    let max = -Infinity; const a = new Array(m);
    for (let i = 0; i < m; i++) { a[i] = choosiness * attractiveness(disp(i), preference, { mode, width }); if (a[i] > max) max = a[i]; }
    let sum = 0; for (let i = 0; i < m; i++) { a[i] = Math.exp(a[i] - max); sum += a[i]; }
    let r = rng() * sum;
    for (let i = 0; i < m; i++) { r -= a[i]; if (r <= 0) return pool ? pool[i] : i; }
    return pool ? pool[m - 1] : m - 1;
  }

  // breed(mother, father, opts) -> an offspring's GENES {display, pref}. Both parents pass BOTH a display allele and a
  //   preference allele (midparent + mutation), so a chooser's taste and a displayer's showiness travel together into
  //   the next generation -- the genetic covariance that IS Fisher's runaway. `mutate` sets the mutation SD; clamps
  //   keep alleles finite. Sex-limited EXPRESSION (display in males, choice in females) is the caller's business; the
  //   GENES for both are carried by every offspring, which is what lets the two traits co-evolve.
  function breed(mother, father, opts) {
    opts = opts || {};
    const rng = opts.rng || Math.random;
    const mut = opts.mutate != null ? opts.mutate : 0.05;
    const lo = opts.min != null ? opts.min : -Infinity, hi = opts.max != null ? opts.max : Infinity;
    const mid = (a, b) => (a + b) / 2;
    return {
      display: clamp(mid(mother.display, father.display) + gauss(rng) * mut, lo, hi),
      pref: clamp(mid(mother.pref, father.pref) + gauss(rng) * mut, lo, hi)
    };
  }

  return { attractiveness, displayCost, choose, breed };
});