Divide the Dollar
Does fair division emerge from blind evolution? Brian Skyrms's classic demonstration that it (mostly) does — his original C program, and how the same model is a handful of lines with the Dynamo toolkit, built up in stages to the full study.
Introduction
In Evolution of the Social Contract (1996) Brian Skyrms asks whether the norm of fair division can arise with no designer, purely through the differential reproduction of strategies. His test case is the Nash demand game, "divide the dollar", run under the replicator dynamic; the finding is that from the great majority of starting populations the system converges on the equal split.
Skyrms wrote a short, fast C program to show it. This page reproduces that program with the Dynamo toolkit — and the point of interest is that, because the toolkit supplies the game, the dynamics and the integrator, the reproduction is shorter than the original, not longer. We start with that minimal version, then build up to the full study.
The game
Two players each demand a whole number of dimes, 0 through 10. If the demands are compatible — they sum to no more than the ten-dime dollar — each gets what they asked for; otherwise both get nothing:
payoff(demand i against demand j) = i if i + j ≤ 10
= 0 otherwise
Every split (i, 10−i) is a Nash equilibrium, as is the equal split (5, 5). Under the
replicator these are the rest points: the equal split, and the polymorphisms in
which a fraction demands i and the rest demand 10−i. Which does evolution pick?
Skyrms’s program in C
Here is Skyrms's source in full. It hand-codes the payoff matrix FIT, then iterates the
discrete replicator map with a background fitness c = 10 — P[i] ← P[i]·(U[i]+c)/(Ū+c)
— from a fixed mix of demands 1, 3, 7 and 9, checking for convergence.
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <float.h>
#include <time.h>
#define G 11
#define ranmax 32769.00000
#define ranmax RAND_MAX
#define NOT_CONVERGED 0
#define CONVERGED 1
main()
{
double FIT[G][G], P[G], U[G], meant, USQ, c, norm;
unsigned long int i, j, trials, tr, tgen, maxt, gen, conv, COUNT[G];
tgen = 0;
meant = 0.0000000;
maxt = 0;
for (i=0; i<G; i++)
COUNT[i]=0;
for (i=0; i<G; i++)
for(j=0; j<G; j++)
if (i+j < G)
FIT[i][j]=i;
else
FIT[i][j]=0.0;
c=10.00;
srand( (unsigned)time( NULL ) );
trials = 0;
for (tr=0; tr<1; tr++)
{
norm = 0.000000000;
for (i=0; i<G; i++)
P[i] = -log((rand()+1.000)/ranmax);
for (i=0; i<G; i++)
norm = norm + P[i];
for (i=0; i<G; i++)
P[i] = P[i]/norm;
for (i=0; i<G; i++)
P[i] = 0.0;
P[3] = 0.25; P[7] = 0.25; P[1] = .25; P[9]=.25;
gen = 0;
conv = NOT_CONVERGED;
do{
printf("\n");
for (i=0; i<G; i++)
printf("%f ", P[i]);
gen = gen + 1;
for (i=0; i<G; i++)
U[i]=0.00;
for (i=0; i<G; i++)
for (j=0; j<G; j++)
U[i] = U[i] + FIT[i][j]*P[j];
USQ=0.00000;
for (i=0; i<G; i++)
USQ = USQ + P[i] * U[i];
for (i=0; i<G; i++)
P[i] = P[i] * ((U[i] + c)/(USQ + c));
if (P[5]>.9999) conv = CONVERGED;
if (P[4] + P[6] > .9999) conv = CONVERGED;
if (P[3] + P[7] > .9999) conv = CONVERGED;
if (P[2] + P[8] > .9999) conv = CONVERGED;
if (P[1] + P[9] > .9999) conv = CONVERGED;
if (P[0] + P[10] > .9999) conv = CONVERGED;
} while (gen < 10000);
/* (conv == NOT_CONVERGED && gen < 1000); */
if (gen > maxt)
maxt = gen;
trials = trials + 1;
tgen = tgen + gen;
meant = ((double)tgen)/((double)trials);
for (i=0; i<G; i++)
if (P[i] > .1)
COUNT[i] = (COUNT[i]+1);
}
printf("Divide a stack of ten dimes - 100,000 trials\n");
printf("mean number of generations to convergence at .9999 level %f, \n", meant);
printf("max number of generations to convergence at .9999 level %d,\n\n", maxt);
for (i=0; i<G; i++)
if (COUNT[i]>=0)
printf("%d, %d\n", i, COUNT[i]);
return 0;
}
The same model, in a few lines
The toolkit removes the scaffolding. The payoff matrix is one expression handed to
Game.symmetric, which carries the expected-payoff machinery; the dynamics are a named
field handed to an integrator, which does the iterating. Two correspondences make the
saving concrete:
Building the game
Skyrms (C)
double FIT[G][G];
for (i = 0; i < G; i++)
for (j = 0; j < G; j++)
FIT[i][j] = (i + j < G) ? i : 0.0; /* compatible? get your demand : nothing */Dynamo (JS)
const A = Array.from({ length: N }, (_, i) =>
Array.from({ length: N }, (_, j) => (i + j <= N - 1 ? i : 0)));
const game = Game.symmetric(A); // …and the payoff machinery comes with itEvolving the population
Skyrms (C) — one generation, by hand
for (i = 0; i < G; i++) U[i] = 0; /* expected payoffs */
for (i = 0; i < G; i++)
for (j = 0; j < G; j++) U[i] += FIT[i][j] * P[j];
for (i = 0; i < G; i++) USQ += P[i] * U[i]; /* mean fitness */
for (i = 0; i < G; i++)
P[i] = P[i] * ((U[i] + c) / (USQ + c)); /* and step forward */Dynamo (JS) — the whole run
// Hand the field to the integrator — it does the iterating for you:
const end = trajectory(game, replicator, start, { tMax: 60 }).states.at(-1)[0];Put together — game, a faithful copy of Skyrms's exact discrete map, and a small experiment — the complete short program is this:
#!/usr/bin/env node
/**
* core.mjs — Skyrms's "divide the dollar", the short way.
*
* The whole model in a handful of lines: the toolkit supplies the game, the
* replicator field and the integrator, so there is no payoff loop and no update
* loop to hand-code. Compare with Skyrms's C in ../converge.c.
*/
import { Game, replicator, trajectory, flow, Random } from '../../src/index.js';
// The demand game: demand i of 10 dimes; get it if i + j ≤ 10, else nothing.
const N = 11;
const A = Array.from({ length: N }, (_, i) => Array.from({ length: N }, (_, j) => (i + j <= N - 1 ? i : 0)));
const game = Game.symmetric(A);
const fmt = (p) => p.map((x) => (x < 5e-3 ? ' · ' : x.toFixed(3))).join(' ');
const skyrms = [[0, 0.25, 0, 0.25, 0, 0, 0, 0.25, 0, 0.25, 0]]; // demands 1,3,7,9
// (1) Reproduce Skyrms's run — one call to the integrator does the iterating.
const end = trajectory(game, replicator, skyrms, { tMax: 60 }).states.at(-1)[0];
console.log('replicator, Skyrms start :', fmt(end)); // → the 3 | 7 polymorphism
// (2) Skyrms's *exact* discrete map, if you want it — the toolkit still gives
// you the fitnesses, so it is four lines, not forty.
let p = skyrms[0], c = 10;
for (let gen = 0; gen < 300; gen++) {
const U = game.payoff([p])[0]; // expected payoff of each demand
const Ubar = p.reduce((s, pi, i) => s + pi * U[i], 0); // mean fitness
p = p.map((pi, i) => (pi * (U[i] + c)) / (Ubar + c)); // discrete replicator
}
console.log('discrete map, Skyrms start:', fmt(p)); // → the same 3 | 7
// (3) The experiment: where do random starts go? `Random.simplex` draws a
// uniform random population, so a trial is a single line.
const rng = new Random(1);
let fair = 0, trials = 5000;
for (let k = 0; k < trials; k++) {
const f = flow(game, replicator, [rng.simplex(N)], 80)[0];
if (f[5] > 0.99) fair++;
}
console.log(`\nfair division from ${trials} uniform random starts: ${(100 * fair / trials).toFixed(0)}%`);
replicator, Skyrms start : · · · 0.429 · · · 0.571 · · ·
discrete map, Skyrms start: · · · 0.429 · · · 0.571 · · ·
fair division from 5000 uniform random starts: 60%
So is the library a win? Skyrms's C is 82 non-blank lines; the program above is 22 lines of code and does more (the continuous run, his exact discrete map, and an experiment). The library is shorter precisely where it should be — there is no payoff loop and no update loop to write. The longer file later in this page is long only because of the hand-drawn SVG figure, not the simulation.
Discrete vs. continuous. Dynamo's replicator is the continuous-time field
ẋ_i = x_i·(U_i − Ū); Skyrms iterates a discrete map. They share rest points and basins
(adding a constant to every payoff leaves the field unchanged), so the result is the same.
Scaling up: the experiment
Skyrms's banner — "divide a stack of ten dimes, 100,000 trials" — points at the real study:
not one run, but many random starts, asking how often each equilibrium is reached. With the
toolkit a trial is one line, because Random.simplex draws a random population for you:
const rng = new Random(1);
const tally = {};
for (let k = 0; k < 100000; k++) {
const end = flow(game, replicator, [rng.simplex(N)], 80)[0]; // random start → rest
tally[bucket(end)] = (tally[bucket(end)] || 0) + 1; // which equilibrium?
}
Run over 100,000 uniform random starts (and, for contrast, with the stick-breaking sampler below), the tally is:
[2] The experiment — random starts under the replicator dynamic
"Divide a stack of ten dimes." Which equilibrium does each start reach?
uniform sampling — 100,000 trials
fair division 5|5 60.8% ██████████████████████████████
polymorphism 4|6 28.1% ██████████████
polymorphism 3|7 9.4% █████
polymorphism 2|8 1.7% █
polymorphism 1|9 0.1%
other 0.0%
stick-breaking sampling — 100,000 trials
fair division 5|5 34.2% █████████████████
polymorphism 4|6 28.5% ██████████████
polymorphism 3|7 19.4% ██████████
polymorphism 2|8 11.9% ██████
polymorphism 1|9 5.8% ███
other 0.2%
→ fair division is common but not inevitable: 61% of uniform starts,
only 34% of stick-breaking starts. The rest is polymorphism. (Skyrms 1996.)
Fair division is the single most common outcome — its ~61% under uniform sampling matches
Skyrms's classic result — but it is far from inevitable. The polymorphisms are genuine
attractors: at the (i, 10−i) split a strict demand earns i, while the equal split trying
to invade earns only 5·i/(10−i) < i for i < 5, so it cannot undercut them.
Scaling up: how you sample
How often you see each outcome depends on how the random start is drawn, because different samplers weight different regions of the simplex. The toolkit offers two off the seeded PRNG:
rng.simplex(N) // uniform on the simplex — concentrates near the centroid
rng.stickBreak(N) // sparse mixtures — explores the edges, where polymorphisms live
- Uniform (
simplex) is the geometrically unbiased measure. It concentrates near the centroid — every demand ≈ 1/11 — deep inside fair division's basin, so fair division wins ~61%. - Stick-breaking (
stickBreak) concentrates on sparse mixtures — the edges and faces of the simplex, where the polymorphic equilibria live — so it lands in their basins far more often, and fair division falls to ~34%.
A cautionary detail. Both draws use Random.random() (a real in [0, 1)). An early
version of this demo mistakenly used Random.next() — the raw 32-bit integer — which
collapsed every "random" start onto the centroid and produced a spurious 100% fair
division. When a result looks too clean, suspect the sampler before the model.
Scaling up: the figure
Finally, the publication figure: the two dynamical faces, and the outcome spectrum by sampler. This is where the full program's length comes from — a hand-built SVG, drawn in the toolkit's house style. The simulation underneath is still the few lines above.
(3, 7) polymorphism from
Skyrms’s fixed start. Bottom: the outcome spectrum over 100,000 random starts — uniform
sampling (light) versus stick-breaking (dark), which finds the polymorphisms far more often.The complete program — divide-the-dollar.mjs (simulation + figure)
#!/usr/bin/env node
/**
* divide-the-dollar.mjs — Brian Skyrms's "Divide the Dollar", via the Dynamo toolkit.
*
* A reproduction of Skyrms's C program (`converge.c`, "Divide a stack of ten
* dimes"): the Nash demand game on eleven strategies — each player demands a
* whole number of dimes, 0…10 — evolved under the replicator dynamic.
*
* payoff(demand i against demand j) = i if i + j ≤ 10 (compatible)
* = 0 otherwise (over-demand → nothing)
*
* Skyrms's question (Evolution of the Social Contract, 1996): does fair division
* — everyone demanding half (5|5) — emerge from blind evolutionary dynamics? The
* answer is a resounding yes: from essentially every interior starting point the
* replicator dynamic carries the population to the equal split. The complementary
* "polymorphisms" (a fraction demanding i and the rest demanding 10−i) are
* equilibria too, but their basins are vanishingly small — reachable only when
* the equal split is excluded from the start, as in Skyrms's own fixed run.
*
* What the toolkit supplies here: `Game.symmetric` builds the demand game and its
* payoff machinery, and the `replicator` field + `flow`/`trajectory` integrators
* carry the population. Skyrms wrote a *discrete* replicator with a background
* fitness `c` (`P[i] ← P[i]·(U[i]+c)/(Ū+c)`); the toolkit's replicator is the
* continuous-time field `ẋ_i = x_i·(U_i − Ū)`. They share rest points and basins
* — adding a constant to every payoff leaves the replicator field unchanged — so
* the picture is the same. Part 1 below runs Skyrms's exact discrete update (so
* this is a faithful reproduction of his code); Part 2 uses the toolkit's
* continuous field for the many-trials experiment.
*
* Original C: ~/Source/converge.c (Brian Skyrms). Run: `node divide-the-dollar.mjs`.
*/
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { writeFileSync } from 'node:fs';
import { Game, replicator, flow, trajectory, uniformState, Random } from '../../src/index.js';
const here = dirname(fileURLToPath(import.meta.url));
// ── The game ────────────────────────────────────────────────────────────────
const N = 11; // demands of 0,1,…,10 dimes
const A = Array.from({ length: N }, (_, i) =>
Array.from({ length: N }, (_, j) => (i + j <= N - 1 ? i : 0))
);
const game = Game.symmetric(A, {
name: 'Divide-the-dollar',
labels: [Array.from({ length: N }, (_, i) => String(i))],
});
/** Name the outcome a distribution has settled on. */
function outcome(p) {
const surv = p.map((v, i) => (v > 0.05 ? i : -1)).filter((i) => i >= 0);
if (surv.length === 1 && surv[0] === 5) return 'fair division (5 | 5)';
if (surv.length === 2 && surv[0] + surv[1] === 10) return `polymorphism (${surv[0]} | ${surv[1]})`;
return `mixture {${surv.join(', ')}}`;
}
/** A compact one-line view of a distribution (a dot for ~zero entries). */
const show = (p) => p.map((v) => (v < 5e-3 ? ' · ' : v.toFixed(3))).join(' ');
// ── Part 1 — faithful reproduction: Skyrms's discrete replicator ──────────────
// His exact update, using the toolkit to compute the fitnesses U_i = (A·p)_i.
function skyrmsStep(p, c = 10) {
const U = game.payoff([p])[0]; // toolkit: expected payoff of each demand
const Ubar = p.reduce((s, pi, i) => s + pi * U[i], 0);
return p.map((pi, i) => (pi * (U[i] + c)) / (Ubar + c));
}
/** Skyrms's convergence test: one demand, or a complementary pair, owns the dollar. */
function converged(p) {
if (p[5] > 0.9999) return true;
for (let i = 0; i <= 4; i++) if (p[i] + p[N - 1 - i] > 0.9999) return true;
return false;
}
function reproduceSkyrms() {
console.log('[1] Faithful reproduction — Skyrms\'s discrete replicator (c = 10)');
console.log(' initial mix: demands 1, 3, 7, 9 at 0.25 each (the equal split, 5, is absent)\n');
let p = Array(N).fill(0);
p[1] = p[3] = p[7] = p[9] = 0.25;
const snapshots = new Set([0, 1, 2, 5, 10, 25, 50, 100, 200, 400, 800]);
let gen = 0;
console.log(' gen ' + Array.from({ length: N }, (_, i) => String(i).padStart(5)).join(' '));
for (; gen < 100000; gen++) {
if (snapshots.has(gen)) console.log(` ${String(gen).padStart(5)} ${show(p)}`);
if (converged(p)) break;
p = skyrmsStep(p);
}
console.log(` ${String(gen).padStart(5)} ${show(p)}`);
console.log(`\n → converged after ${gen} generations to ${outcome(p)}.`);
console.log(' With the equal split absent, fair division is unreachable, so the');
console.log(' population settles on the complementary pair that exhausts the dollar.\n');
}
// ── Part 2 — the experiment: many random starts under the replicator field ────
// "Divide a stack of ten dimes — N trials": draw a random initial population,
// carry it to rest with the toolkit's continuous replicator, and record which
// equilibrium it reaches. The starts come straight off the toolkit's seeded PRNG:
// • rng.simplex(N) — uniform on the simplex; piles up near the centroid
// (every demand ≈ 1/11), deep inside fair division's basin.
// • rng.stickBreak(N) — sparse mixtures on the simplex edges and faces, where
// the polymorphic equilibria live, so it finds them far
// more often (fair division drops sharply).
function settle(p0, { T = 800, dt = 0.1, chunk = 5 } = {}) {
let p = [p0];
for (let t = 0; t < T; t += chunk) {
p = flow(game, replicator, p, chunk, { dt });
if (converged(p[0])) break;
}
return p[0];
}
// The outcome spectrum: fair division, the four genuine polymorphisms, and a
// catch-all (rare near-vertex starts the flow cannot leave within the horizon).
const SPECTRUM = [
{ key: 'fair', label: 'fair division 5|5', tick: '5|5' },
{ key: 'p46', label: 'polymorphism 4|6', tick: '4|6' },
{ key: 'p37', label: 'polymorphism 3|7', tick: '3|7' },
{ key: 'p28', label: 'polymorphism 2|8', tick: '2|8' },
{ key: 'p19', label: 'polymorphism 1|9', tick: '1|9' },
{ key: 'other', label: 'other', tick: 'other' },
];
function bucket(p) {
const s = p.map((v, i) => (v > 0.05 ? i : -1)).filter((i) => i >= 0);
if (s.length === 1 && s[0] === 5) return 'fair';
if (s.length === 2 && s[0] + s[1] === 10 && s[0] >= 1 && s[0] <= 4) return `p${s[0]}${s[1]}`;
return 'other';
}
function runExperiment(draw, name, trials, seed) {
const rng = new Random(seed);
const count = Object.fromEntries(SPECTRUM.map((c) => [c.key, 0]));
for (let k = 0; k < trials; k++) count[bucket(settle(draw(rng)))]++;
const pct = (k) => (100 * count[k]) / trials;
console.log(` ${name} sampling — ${trials.toLocaleString()} trials`);
for (const c of SPECTRUM) {
const p = pct(c.key);
console.log(` ${c.label.padEnd(18)} ${p.toFixed(1).padStart(5)}% ${'█'.repeat(Math.round(p / 2))}`);
}
console.log('');
return { name, trials, count, pct };
}
// ── The shareable figure ──────────────────────────────────────────────────────
// Two stories side by side: fair division from a uniform start, and Skyrms's
// polymorphism from his fixed start. Hand-built SVG in the toolkit's house style.
const PAL = { paper: '#faf9f7', ink: '#2c2c2c', ink2: '#6b6b6b', accent: '#8b7355', grid: '#e6e1d8', faint: '#cdc6ba' };
const SURV = ['#8b7355', '#4a5a6b', '#8b4a4a', '#4a6b5a', '#b8965a'];
function series(p0, tMax) {
const tr = trajectory(game, replicator, [p0], { tMax, dt: 0.05 });
const samples = tr.states.filter((_, i) => i % 4 === 0 || i === tr.states.length - 1);
const final = tr.states[tr.states.length - 1][0];
return { rows: samples.map((s) => s[0]), tMax, final };
}
function panel(x0, y0, w, h, data, title) {
const padL = 44, padB = 30, padT = 30, padR = 64;
const px = x0 + padL, py = y0 + padT, pw = w - padL - padR, ph = h - padT - padB;
const X = (t) => px + (t / (data.rows.length - 1)) * pw;
const Y = (v) => py + (1 - v) * ph;
const out = [];
out.push(`<text x="${x0 + padL}" y="${y0 + 18}" font-family="'Crimson Pro',Georgia,serif" font-size="17" fill="${PAL.ink}">${title}</text>`);
// axes + gridlines
for (let g = 0; g <= 1; g += 0.25) {
out.push(`<line x1="${px}" y1="${Y(g).toFixed(1)}" x2="${px + pw}" y2="${Y(g).toFixed(1)}" stroke="${PAL.grid}" stroke-width="1"/>`);
out.push(`<text x="${px - 8}" y="${(Y(g) + 3.5).toFixed(1)}" text-anchor="end" font-family="'JetBrains Mono',monospace" font-size="10" fill="${PAL.ink2}">${g.toFixed(2)}</text>`);
}
out.push(`<line x1="${px}" y1="${py}" x2="${px}" y2="${py + ph}" stroke="${PAL.ink}" stroke-width="1.2"/>`);
out.push(`<line x1="${px}" y1="${py + ph}" x2="${px + pw}" y2="${py + ph}" stroke="${PAL.ink}" stroke-width="1.2"/>`);
out.push(`<text x="${px + pw / 2}" y="${y0 + h - 4}" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="10" fill="${PAL.ink2}">time →</text>`);
// one line per demand; dying demands faint, survivors strong + labelled
const survivors = data.final.map((v, i) => (v > 0.05 ? i : -1)).filter((i) => i >= 0);
let ci = 0;
for (let i = 0; i < N; i++) {
const isSurv = survivors.includes(i);
const col = isSurv ? SURV[ci++ % SURV.length] : PAL.faint;
const d = data.rows.map((r, k) => `${k === 0 ? 'M' : 'L'}${X(k).toFixed(1)},${Y(r[i]).toFixed(1)}`).join('');
out.push(`<path d="${d}" fill="none" stroke="${col}" stroke-width="${isSurv ? 2.4 : 1}" opacity="${isSurv ? 1 : 0.6}"/>`);
if (isSurv) {
const yEnd = Y(data.final[i]);
out.push(`<text x="${px + pw + 6}" y="${(yEnd + 3.5).toFixed(1)}" font-family="'JetBrains Mono',monospace" font-size="11" fill="${col}">demand ${i}</text>`);
}
}
return out.join('');
}
/** Grouped bars: the outcome spectrum for the two sampling methods. */
function barPanel(x0, y0, w, h, uniform, stick, title) {
const padL = 42, padB = 30, padT = 30, padR = 16;
const px = x0 + padL, py = y0 + padT, pw = w - padL - padR, ph = h - padT - padB;
const top = 70; // % axis maximum
const Y = (v) => py + (1 - v / top) * ph;
const out = [];
out.push(`<text x="${px}" y="${y0 + 18}" font-family="'Crimson Pro',Georgia,serif" font-size="17" fill="${PAL.ink}">${title}</text>`);
for (let g = 0; g <= top; g += 10) {
out.push(`<line x1="${px}" y1="${Y(g).toFixed(1)}" x2="${px + pw}" y2="${Y(g).toFixed(1)}" stroke="${PAL.grid}" stroke-width="1"/>`);
out.push(`<text x="${px - 7}" y="${(Y(g) + 3.5).toFixed(1)}" text-anchor="end" font-family="'JetBrains Mono',monospace" font-size="10" fill="${PAL.ink2}">${g}%</text>`);
}
out.push(`<line x1="${px}" y1="${(py + ph).toFixed(1)}" x2="${px + pw}" y2="${(py + ph).toFixed(1)}" stroke="${PAL.ink}" stroke-width="1.2"/>`);
const slot = pw / SPECTRUM.length, bw = slot * 0.30;
SPECTRUM.forEach((c, i) => {
const cx = px + slot * (i + 0.5);
const bar = (bx, val, col) =>
`<rect x="${bx.toFixed(1)}" y="${Y(val).toFixed(1)}" width="${bw.toFixed(1)}" height="${(py + ph - Y(val)).toFixed(1)}" fill="${col}"/>` +
`<text x="${(bx + bw / 2).toFixed(1)}" y="${(Y(val) - 4).toFixed(1)}" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="9.5" fill="${PAL.ink2}">${val.toFixed(0)}</text>`;
out.push(bar(cx - bw - 2, uniform.pct(c.key), PAL.faint));
out.push(bar(cx + 2, stick.pct(c.key), PAL.accent));
out.push(`<text x="${cx.toFixed(1)}" y="${(py + ph + 16).toFixed(1)}" text-anchor="middle" font-family="'JetBrains Mono',monospace" font-size="11.5" fill="${PAL.ink}">${c.tick}</text>`);
});
const lx = px + pw - 168, ly = py + 2;
out.push(`<rect x="${lx}" y="${ly}" width="11" height="11" fill="${PAL.faint}"/><text x="${lx + 16}" y="${ly + 10}" font-family="'JetBrains Mono',monospace" font-size="11" fill="${PAL.ink2}">uniform</text>`);
out.push(`<rect x="${lx + 84}" y="${ly}" width="11" height="11" fill="${PAL.accent}"/><text x="${lx + 100}" y="${ly + 10}" font-family="'JetBrains Mono',monospace" font-size="11" fill="${PAL.ink2}">stick-break</text>`);
return out.join('');
}
function writeFigure(uniformData, skyrmsData, uniform, stick) {
const W = 980, H = 712, pw = 460, ph = 250, top = 64;
const cap = 'Fair division is common but not inevitable — the polymorphisms (i | 10−i) take the rest, and stick-breaking finds them far more often.';
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="'Crimson Pro',Georgia,serif">` +
`<style>@import url('https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600&family=JetBrains+Mono&display=swap');</style>` +
`<rect width="${W}" height="${H}" fill="${PAL.paper}"/>` +
`<text x="${W / 2}" y="30" text-anchor="middle" font-size="22" font-weight="600" fill="${PAL.ink}">Divide the Dollar — the replicator dynamic</text>` +
`<text x="${W / 2}" y="50" text-anchor="middle" font-size="13" fill="${PAL.ink2}">After Brian Skyrms · reproduced with the Dynamo toolkit</text>` +
panel(20, top, pw, ph, uniformData, 'A uniform start → fair division') +
panel(W - pw - 20, top, pw, ph, skyrmsData, 'Skyrms’s start {1,3,7,9} → polymorphism') +
barPanel(20, top + ph + 30, W - 40, 270, uniform, stick, `Outcomes of ${uniform.trials.toLocaleString()} random starts`) +
`<text x="${W / 2}" y="${H - 14}" text-anchor="middle" font-size="13.5" fill="${PAL.ink}">${cap}</text>` +
`</svg>`;
const file = join(here, 'divide-the-dollar.svg');
writeFileSync(file, svg);
return file;
}
// ── Main ──────────────────────────────────────────────────────────────────────
const trials = Number(process.argv[2]) || 2000;
const seed = Number(process.argv[3]) || 20260607;
console.log('Skyrms — Divide the Dollar (ten dimes), via the Dynamo toolkit');
console.log('='.repeat(62));
console.log('Nash demand game · 11 strategies (demand 0…10)');
console.log('payoff(i vs j) = i if i + j ≤ 10, else 0 (over-demand → nothing)\n');
reproduceSkyrms();
console.log('[2] The experiment — random starts under the replicator dynamic');
console.log(' "Divide a stack of ten dimes." Which equilibrium does each start reach?\n');
const uniform = runExperiment((rng) => rng.simplex(N), 'uniform', trials, seed);
const stick = runExperiment((rng) => rng.stickBreak(N), 'stick-breaking', trials, seed + 1);
console.log(` → fair division is common but not inevitable: ${uniform.pct('fair').toFixed(0)}% of uniform starts,`);
console.log(` only ${stick.pct('fair').toFixed(0)}% of stick-breaking starts. The rest is polymorphism. (Skyrms 1996.)\n`);
const file = writeFigure(series(uniformState([N])[0], 18), series(skyrmsIC(), 28), uniform, stick);
console.log(`Wrote figure → ${file}`);
function skyrmsIC() { const p = Array(N).fill(0); p[1] = p[3] = p[7] = p[9] = 0.25; return p; }
References
- B. Skyrms, Evolution of the Social Contract (Cambridge University Press, 1996), ch. 1.
- B. Skyrms, "Sex and Justice", Journal of Philosophy 91 (1994): 305–320.
- Original source:
converge.c(Brian Skyrms), included alongside this page. - Toolkit pieces used:
Game.symmetric,replicator,trajectory/flow,Random.simplexandRandom.stickBreak.