← Natura·Docs/Modules/Source

verbs/court-test.js

121 lines · in natura-src-1.0.0.zip as natura/verbs/court-test.js · a test

// court-test.js -- assertions for the COURTSHIP / MATE-CHOICE verb. The primitives are checked directly; the big
// claim -- DIRECTIONAL SEXUAL SELECTION and its CHECK by the handicap cost -- is proven by RUNNING a small
// generational GA on the verb, never by reasoning (the balance-trap discipline).
const N = require('../natura.js');
const court = require('./court.js');
const { attractiveness, displayCost, choose, breed } = court;
const { rngFrom } = N;

let pass = 0, fail = 0;
const ok = (c, m) => { if (c) pass++; else { fail++; console.log('  FAIL: ' + m); } };
console.log('court assertions:');

// 1. displayCost -- monotone increasing + convex (a bigger display costs disproportionately -> an honest handicap)
{
  const c1 = displayCost(1), c2 = displayCost(2), c3 = displayCost(3);
  ok(c1 < c2 && c2 < c3, 'display cost rises with the display');
  ok((c3 - c2) > (c2 - c1), 'and it is convex -- each extra unit of showiness costs more than the last');
  ok(displayCost(0) === 0, 'no display, no cost');
}

// 2. attractiveness -- directional (bigger better, scaled by taste) vs matching (peaks at the preferred value)
{
  ok(attractiveness(2, 1) > attractiveness(1, 1), 'directional: a bigger display is more attractive');
  ok(attractiveness(2, 2) > attractiveness(2, 1), 'a stronger preference values the same display more');
  const m1 = attractiveness(1.0, 1.0, { mode: 'matching', width: 0.3 });
  const m2 = attractiveness(1.6, 1.0, { mode: 'matching', width: 0.3 });
  ok(m1 > m2, 'matching: a display near the preferred value beats one far from it (a tuned, stabilising taste)');
}

// 3. choose -- choosy pickers favour the showiest; choosiness 0 is random mating (no sexual selection)
{
  const rng = rngFrom(11);
  const cands = [{ display: 0.2 }, { display: 0.5 }, { display: 2.5 }, { display: 0.8 }];  // index 2 is the showiest
  let topPick = 0, n = 4000;
  for (let i = 0; i < n; i++) if (choose(1, cands, { choosiness: 4, rng }) === 2) topPick++;
  ok(topPick / n > 0.85, 'a choosy picker takes the showiest most of the time (' + (topPick / n).toFixed(2) + ')');
  const rng2 = rngFrom(11); let topRandom = 0;
  for (let i = 0; i < n; i++) if (choose(1, cands, { choosiness: 0, rng: rng2 }) === 2) topRandom++;
  ok(Math.abs(topRandom / n - 0.25) < 0.04, 'choosiness 0 = RANDOM mating, no bias to the showy (' + (topRandom / n).toFixed(2) + ' ~ 0.25)');
}

// 4. breed -- offspring genes are midparent + a little mutation; deterministic under a seeded rng
{
  const rng = rngFrom(7);
  const kid = breed({ display: 1, pref: 2 }, { display: 3, pref: 4 }, { mutate: 0, rng });
  ok(Math.abs(kid.display - 2) < 1e-9 && Math.abs(kid.pref - 3) < 1e-9, 'no mutation -> exact midparent (display 2, pref 3)');
  const a = breed({ display: 1, pref: 1 }, { display: 1, pref: 1 }, { mutate: 0.1, rng: rngFrom(3) });
  const b = breed({ display: 1, pref: 1 }, { display: 1, pref: 1 }, { mutate: 0.1, rng: rngFrom(3) });
  ok(a.display === b.display && a.pref === b.pref, 'same seed -> identical offspring (reproducible)');
}

// ---- the GA that runs the verb over generations. One round of natural selection (survival taxed by the display's
//      handicap cost), then choice-driven reproduction: each surviving mother secures a father by HER preference, and
//      her fecundity rises with the display of the mate she secures (the sexy-son feedback -- securing a showy mate
//      pays in grand-offspring), so the choosers' pull on the display compounds across generations. ----
function evolve(cfg) {
  const rng = rngFrom(cfg.seed || 1);
  const NN = cfg.N || 400, gens = cfg.gens || 100, k = cfg.cost, choosiness = cfg.choosiness, mut = cfg.mutate || 0.06;
  let pop = [];
  for (let i = 0; i < NN; i++) pop.push({ display: 0.1 + 0.05 * rng(), pref: 1.0 + 0.6 * (rng() - 0.5) });
  for (let g = 0; g < gens; g++) {
    const surv = pop.filter(ind => rng() < Math.max(0.05, 1 - displayCost(ind.display, { k })));  // the handicap kills the showy
    const pool = surv.length ? surv : pop;
    const pairs = pool.map(mom => { const fi = choose(mom.pref, pool, { choosiness, rng, sample: Math.min(15, pool.length) }); return { mom, dad: pool[fi >= 0 ? fi : 0] }; });
    let tot = 0; const w = pairs.map(p => { const x = 0.15 + p.dad.display; tot += x; return x; });  // fecundity ~ secured mate's display
    const next = [];
    for (let i = 0; i < NN; i++) { let r = rng() * tot, pi = 0; for (; pi < w.length; pi++) { r -= w[pi]; if (r <= 0) break; } const p = pairs[Math.min(pi, pairs.length - 1)]; next.push(breed(p.mom, p.dad, { mutate: mut, rng, min: 0, max: 40 })); }
    pop = next;
  }
  const mean = key => pop.reduce((s, x) => s + x[key], 0) / pop.length;
  return { display: mean('display'), pref: mean('pref') };
}

// 5. E1 -- THE RUNAWAY: with choosy choosers and heredity, the display ESCALATES far past its ~0.1 start -- a trait
//    grown not because it helps survival (it hurts) but because it is CHOSEN.
{
  const r = evolve({ cost: 0.0008, choosiness: 6, seed: 5 });
  ok(r.display > 2.0, 'the display RUNS AWAY far past its ~0.1 start under choice (mean ' + r.display.toFixed(2) + ')');
}

// 6. E2 -- THE CHECK: the handicap cost holds the runaway to a finite equilibrium; a heavier cost -> a smaller display
//    (natural selection checking sexual selection -- the peacock's tail is as big as survival will bear, no bigger).
{
  const cheap = evolve({ cost: 0.0005, choosiness: 6, seed: 5 });
  const dear = evolve({ cost: 0.15, choosiness: 6, seed: 5 });
  ok(cheap.display > dear.display + 0.25, 'a costlier display settles SMALLER (cheap ' + cheap.display.toFixed(2) + ' vs dear ' + dear.display.toFixed(2) + ') -- survival checks the runaway');
}

// 7. E3 -- CHOICE IS THE DRIVER: kill the choice (choosiness 0 = random mating) and the display does NOT run away,
//    though everything else is identical -- the escalation is the PREFERENCE's doing, not survival's.
{
  const chosen = evolve({ cost: 0.01, choosiness: 6, seed: 5 });
  const random = evolve({ cost: 0.01, choosiness: 0, seed: 5 });
  ok(chosen.display > random.display + 1.0, 'with choice the display grows large; with RANDOM mating it stays small (' + chosen.display.toFixed(2) + ' vs ' + random.display.toFixed(2) + ')');
}

// 8. E4 -- THE TWO KINDS OF TASTE: a MATCHING (recognition) preference is STABILISING, not runaway. Females prefer a
//    display NEAR a target value; the display converges TO that value and HOLDS there, and TRACKS it if it shifts --
//    the opposite pole from the open-ended directional taste (E1), which escalates without bound. Both from one verb.
{
  function evolveMatch(pstar, width, seed) {
    const rng = rngFrom(seed); const NN = 400, gens = 180, ch = 5, mut = 0.05;
    let pop = []; for (let i = 0; i < NN; i++) pop.push({ display: 0.3 + 0.1 * rng(), pref: pstar });
    for (let g = 0; g < gens; g++) {
      const pool = pop;
      const pairs = pool.map(mom => { const fi = choose(mom.pref, pool, { choosiness: ch, rng, mode: 'matching', width, sample: 18 }); return { mom, dad: pool[fi >= 0 ? fi : 0] }; });
      let tot = 0; const w = pairs.map(p => { const x = 0.03 + attractiveness(p.dad.display, p.mom.pref, { mode: 'matching', width }); tot += x; return x; });
      const next = [];
      for (let i = 0; i < NN; i++) { let r = rng() * tot, pi = 0; for (; pi < w.length; pi++) { r -= w[pi]; if (r <= 0) break; } const p = pairs[Math.min(pi, pairs.length - 1)]; const kid = breed(p.mom, p.dad, { mutate: mut, rng, min: 0, max: 30 }); kid.pref = pstar; next.push(kid); }
      pop = next;
    }
    return pop.reduce((s, x) => s + x.display, 0) / pop.length;
  }
  const low = evolveMatch(0.7, 0.6, 5), high = evolveMatch(1.4, 0.6, 5);
  ok(Math.abs(low - 0.7) < 0.25, 'a MATCHING preference for ~0.7 pulls the display TO ~0.7 and holds -- stabilising, not runaway (' + low.toFixed(2) + ')');
  ok(Math.abs(high - 1.4) < 0.25, 'shift the preferred value up to ~1.4 and the display TRACKS it (' + high.toFixed(2) + ')');
  ok(high > low + 0.4 && high < 2.0, 'the display sits AT the preferred value, never running away past it (vs the directional runaway to 2.6+) -- the second kind of sexual selection, from the same verb');
}

console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);