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