# Dynamo.js — conventions & lessons (the contributor's brief)

This document is the single source of truth for how Dynamo.js is built. Anyone —
human or sub-agent — extending the library, converting more of Sandholm's
notebooks, or writing documentation **must follow it**. It records both the
architecture and the hard-won lessons.

---

## 1. What this is

A faithful, zero-dependency JavaScript port of **William H. Sandholm's *Dynamo***
Mathematica suite (with Emin Dokumacı and Francisco Franchetti), which draws phase
diagrams of evolutionary game dynamics. Built in Sandholm's memory.

The crucial realisation: the five Dynamo notebooks (`2x2`, `3S`, `4S`, `3x2`,
`2x2x2`) share **one identical engine** — the same dynamics, the same payoff
machinery — and differ **only in geometry**. So Dynamo.js is *one* general engine
plus per-geometry renderers, not five ports.

---

## 2. Architecture

```
src/
  prng.js        seeded PRNG (xoshiro128**) — the ONLY source of randomness
  linalg.js      vectors, matrices, solveLinear, eig2/eig3/eigenvalues, jacobianFD
  state.js       state representation: full ⇄ reduced ⇄ flat; simplex constraint
  game.js        Game (payoff field F) + payoff machinery + preset catalogue
  dynamics.js    the 14 evolutionary dynamics, as V(state, game) → velocity
  integrator.js  RK4 + adaptive Dormand–Prince RK45; trajectory(), flow()
  geometry.js    maps a state to a drawable shape (square/triangle/tetra/cube/prism)
  rest-points.js zeros of the field (N-D Newton on the simplex) + Nash/ESS
  stability.js   eigenvalue + radial-contraction classification of rest points
  colormap.js    speed heat-map colour scales
  render-2d.js   SVG phase portraits for the 2-D shapes
  render-3d.js   SVG (rotatable, projected) phase portraits for the 3-D shapes
  index.js       public barrel
build.js         hand-rolled bundler → dist/dynamo.js (single file, `Dynamo` global)
test/            hand-rolled test harness; one suite per module
```

**Hard separation (load-bearing):** the engine owns no rendering; the renderers
own no simulation logic. A dynamic is a pure function `(state, game) => velocity`.
This is the same discipline the parent TROCP toolkit insists on.

---

## 3. The state model (the key abstraction)

A game is fixed by `dims` — the strategy count of each population:

| dims | populations × strategies | shape |
|---|---|---|
| `[2,2]` | 2 × 2 | square |
| `[3]` | 1 × 3 | triangle |
| `[4]` | 1 × 4 | tetrahedron |
| `[3,2]` | 2 × {3,2} | prism |
| `[2,2,2]` | 3 × 2 | cube |

- **Full state**: one mixed strategy per population, e.g. `[[x₁,x₂],[y₁,y₂]]`,
  each row summing to 1.
- **Reduced state**: drop the last coordinate of each population (it's `1 − Σ`),
  giving the `Σ(nₚ−1)`-dimensional point that is integrated, root-found and drawn.
  `fullToReduced` / `reducedToFull` (in `state.js`) are general over `dims`.
- A **velocity** has the same shape as a full state and is simplex-tangent
  (`Σⱼ ẋₚⱼ = 0` for every population). This invariant is tested for every dynamic.

**Lesson:** anything that extracts "the reduced part" must use `fullToReduced`, not
a hand-written `[v[0][0], v[1][0]]` — the latter silently assumes two populations.

---

## 4. The dynamics (the canonical reference)

All transcribed from the notebook and cross-checked against Sandholm,
*Population Games and Evolutionary Dynamics* (MIT Press, 2010). `F̄ₚ` = mean payoff,
`F̂ₚⱼ = Fₚⱼ − F̄ₚ` = excess payoff, `[·]₊` = positive part.

| dynamic | formula | RP / S |
|---|---|---|
| `replicator` | `ẋₚⱼ = xₚⱼ·F̂ₚⱼ` | Automatic / Smooth |
| `maynardSmith` | `ẋₚ = (1/F̄ₚ)·replicatorₚ` | Automatic / Smooth |
| `logit(η)` | `softmax(Fₚ/η)ⱼ − xₚⱼ` | Automatic / Smooth |
| `iLogit(η)` | `xₚⱼ·e^{Fₚⱼ/η}/Σₖxₚₖe^{Fₚₖ/η} − xₚⱼ` | Automatic / HiperESS |
| `br` | `logit(0.001)` | Nash / ESS |
| `temperedLogit(η,Q)` | Zusai tempered logit of `Q(F)` | Automatic / Unknown |
| `temperedBR(Q)` | `temperedLogit(0.001, Q)` | Nash / Unknown |
| `bnn` | `[F̂ₚⱼ]₊ − xₚⱼ·Σₖ[F̂ₚₖ]₊` | Nash / ESS |
| `smith` | `Σₖ xₚₖ[Fₚⱼ−Fₚₖ]₊ − xₚⱼΣₖ[Fₚₖ−Fₚⱼ]₊` | Nash / ESS |
| `excessPayoff` | `σ̃ₚⱼ − xₚⱼ·Σₖσ̃ₚₖ`, `σ̃=([F̂]₊)²` | Nash / ESS |
| `projection` | tangent-cone projection of `F` (active-set) | Nash / ESS |
| `sampleBR(k)` | `E[BR(k-sample)] − x` (exact expectation) | Automatic / Smooth |
| `selMut(M)` | `Mᵀ·diag(x)·F − (x·F)x` | Automatic / Unknown |
| `combine(A,B,wA,wB)` | `wA·A + wB·B` | Nash / Unknown |

Each field carries `.characterisation = { rp, s, ao }`. The `s` value selects the
stability test: `Smooth` → eigenvalues of the Jacobian; otherwise → the
radial-contraction test (robust to the kinks in BNN/Smith/BR/projection, and to
their *polynomial* convergence near a strict Nash, which a fixed-horizon
integration test misses).

**Numerical lessons:** use a **stable soft-max** (subtract the max before `exp`)
so `logit(0.001)` cannot overflow. `sampleBR` is the *deterministic mean* dynamic
(a multinomial expectation), not a simulation. `maynardSmith` divides by `F̄ₚ` and
needs positive payoffs.

---

## 5. Conventions (non-negotiable)

- **British English** everywhere — colour, behaviour, normalise, neighbouring,
  centre — in code, comments, docs and commit messages.
- **Zero runtime dependencies.** No frameworks, no D3, no Chart.js. Hand-built SVG.
- **Seeded randomness only.** Everything stochastic goes through `prng.js`; never
  `Math.random()` in engine code.
- **Renderers return SVG strings** (pure, testable in Node, identical in the
  browser). This is the `renderToSVG()` discipline: the print/export path is the
  primary path. 3-D shapes are an orthographic projection re-rendered per frame.
- **Naming:** lowercase-hyphenated files; camelCase exports; Greek/Latin maths
  names where conventional (`eta`, not `noiseLevel`).
- **Scholarly aesthetic** (the BTT design guide): Crimson Pro (serif) + JetBrains
  Mono (code/labels); warm ink/paper palette; accent `#8b7355`. Restraint over
  decoration.

---

## 6. Faithfulness method (how to port more)

1. **Locate** the definition in the `.nb` (box form `BoxData[RowBox[...]]`; find
   the content/stylesheet boundary with `grep -n 'StyleData\['`).
2. **Transcribe** it exactly, citing the notebook line number in a comment.
3. **Cross-check** against the published formula (Sandholm 2010).
4. **Pin a test**: a hand-computed value at a known state, plus the universal
   invariants (simplex-tangency; rest only at the expected points).
5. Validate against **known theory** (Hawk–Dove two-population *saddle* interior;
   Matching Pennies *centre*; RPS *centre*; coordination *sinks*), never just
   self-consistency.

---

## 7. Testing

Hand-rolled harness (`test/harness.js`): `group / assert / assertClose /
assertDeepClose / summary`. One suite per module, `*.test.js`, each a standalone
Node process; `npm test` runs them all via `test/run-tests.js`. Add tests with
every feature. Never mark work done with a failing or absent test.

---

## 8. Instructions for sub-agents

- Read this file first; obey every convention above.
- Touch only the files you are assigned; do not refactor unrelated code.
- For documentation: be accurate to the **actual** API (read `src/index.js` and the
  module each export lives in). Do not invent functions. Use British English.
- For ports/maths: transcribe faithfully, cite notebook lines, pin a test.
- The shared engine maths is already written and verified — do not re-derive it.
- Return concrete, verifiable output; quote real code; cite line numbers.

---

## 9. Public API (current surface)

`Game` (`.normalForm`, `.symmetric`, `.linear`, `.trilinear`, `.nonlinear`;
`.payoff/.mean/.excess/.excessPlus/.projected`), `standardGames`, `gameCatalog`,
`projectionMatrix`; the 14 dynamics + `dynamics` registry + `bestResponse`;
`trajectory`, `flow`; `restPoints`, `nashEquilibria`, `isNash`, `isStrictNash`;
`classify`, `classifyAll`, `classifyEigen`, `jacobian`, `numericStability`;
`geometryFor`; `phasePortraitSVG`, `phasePortrait3dSVG`, `viewTransform`;
`heatColor`, `scaleStops`; `Random`, `setSeed`, `random`; the linear-algebra and
state helpers. Bundled as `dist/dynamo.js` exposing a `Dynamo` global.
