§1Overview

This library provides a complete JavaScript implementation of the evolutionary dynamics from William Sandholm's Dynamo software. It covers the five major families of population dynamics — imitative, excess payoff, pairwise comparison, perturbed best response, and target — together with an adaptive numerical integrator, Lyapunov functions for convergence analysis, and simplex geometry utilities.

The design follows a strict observer/model separation: all functions are pure (no DOM, no side effects), operating on plain arrays. A population state $\mathbf{x} = (x_1, \ldots, x_n)$ is an array of length $n$ summing to 1. A payoff function $F$ maps states to payoff vectors of the same length. Each dynamic is a function $(\mathbf{x}, F) \to \dot{\mathbf{x}}$ returning a velocity in the tangent space of the simplex.

Dynamic Family Export Rest Points
Replicator Imitative replicator NE + faces
Maynard Smith Replicator Imitative maynardSmithReplicator NE + faces
BNN Excess Payoff bnn Nash equilibria
Smith Pairwise smith Nash equilibria
Logit Perturbed BR logit(η) Logit equilibria
Independent Logit Perturbed BR ilogit(η) Logit equilibria
Best Response Perturbed BR bestResponse() Nash equilibria
Projection Target projection Nash equilibria

§2Getting Started

Installation

The library is a single ES module with no dependencies. Copy evolutionary-dynamics.js into your project and import what you need:

import {
  normalFormPayoff, replicator, bnn, smith, logit,
  integrate, baryToCart, simplexGrid
} from './evolutionary-dynamics.js';

Basic Example

Compute a trajectory of the replicator dynamic on Rock–Paper–Scissors:

// Define the game
const A = [
  [ 0, -1,  1],
  [ 1,  0, -1],
  [-1,  1,  0]
];
const F = normalFormPayoff(A);

// Integrate from an initial condition
const trajectory = integrate(replicator, F,
  [0.6, 0.3, 0.1],  // x₀
  50,                  // T = 50 time units
  { outputInterval: 0.1 }
);

// Convert to 2D coordinates for plotting
for (const { t, x } of trajectory) {
  const [px, py] = baryToCart(x);
  // draw point at (px, py)...
}

Velocity Fields

Compute the velocity field on a grid for visualisation:

import { velocityField, baryToCart } from './evolutionary-dynamics.js';

const field = velocityField(replicator, F, 20);  // 20 subdivisions

for (const { x, v } of field) {
  const [px, py] = baryToCart(x);
  const [vx, vy] = baryToCart(v);
  // draw arrow from (px, py) with direction (vx, vy)...
}

§3Interactive Demo

Click anywhere on the simplex to launch a trajectory from that initial condition. The three vertices represent the three pure strategies. Edit the payoff matrix or choose a preset game to explore different phase portraits.

§4Dynamics Reference

Replicator Dynamic

$$\dot{x}_j = x_j \bigl[ F_j(\mathbf{x}) - \bar{F}(\mathbf{x}) \bigr]$$

The canonical imitative dynamic. Each strategy grows in proportion to its current share times its excess payoff over the population average $\bar{F}(\mathbf{x}) = \mathbf{x} \cdot F(\mathbf{x})$. Rest points include all faces of the simplex plus any interior Nash equilibria. In potential games the replicator converges to Nash equilibria; in zero-sum games like Rock–Paper–Scissors it cycles.

import { replicator } from './evolutionary-dynamics.js';
const dxdt = replicator(x, F);  // velocity at state x

Maynard Smith Replicator

$$\dot{x}_j = \frac{x_j \bigl[ F_j(\mathbf{x}) - \bar{F}(\mathbf{x}) \bigr]}{\bar{F}(\mathbf{x})}$$

The replicator in continuous-time Maynard Smith form, normalised by average fitness. Requires all payoffs to be positive (add a constant to the payoff matrix if needed). Orbits are identical to the standard replicator, but the speed along orbits differs.

BNN Dynamic

$$\dot{x}_j = \bigl[\hat{F}_j(\mathbf{x})\bigr]^{+} - x_j \sum_{k} \bigl[\hat{F}_k(\mathbf{x})\bigr]^{+}$$

Brown–von Neumann–Nash (1950). An excess payoff dynamic where agents switch toward strategies with positive excess payoff $[\hat{F}_j]^{+} = \max(0, F_j - \bar{F})$. Rest points are exactly the Nash equilibria. In stable games the Lyapunov function $V(\mathbf{x}) = \sum_j [\hat{F}_j]^{+2}$ ensures convergence.

Smith Dynamic

$$\dot{x}_j = \sum_{k} x_k \, \bigl[F_j(\mathbf{x}) - F_k(\mathbf{x})\bigr]^{+} - x_j \sum_{k} \bigl[F_k(\mathbf{x}) - F_j(\mathbf{x})\bigr]^{+}$$

Smith (1984). A pairwise comparison dynamic: agents compare their payoff with a randomly encountered opponent and switch if better off. The conditional switch rate $\rho_{jk} = [F_j - F_k]^{+}$ gives the rate of switching from $k$ to $j$. Rest points are Nash equilibria.

Logit Dynamic

$$\dot{x}_j = \frac{\exp(F_j(\mathbf{x})/\eta)}{\sum_{k} \exp(F_k(\mathbf{x})/\eta)} - x_j$$

Fudenberg & Levine (1998). A perturbed best response dynamic where agents drift toward the logit (softmax) choice distribution. The noise parameter $\eta > 0$ controls the precision of choice: as $\eta \to 0$ the dynamic approaches the best response dynamic; as $\eta \to \infty$ it approaches uniform randomisation. Rest points are logit equilibria — perturbed versions of Nash equilibria.

const dyn = logit(0.3);     // create logit with η = 0.3
const dxdt = dyn(x, F);       // evaluate at state x

Projection Dynamic

$$\dot{\mathbf{x}} = \Pi_{T_{\Delta}(\mathbf{x})} \bigl(F(\mathbf{x})\bigr)$$

Nagurney & Zhang (1997). Projects the payoff vector onto the tangent cone of the simplex at the current state. On the interior this is simply the mean-centred payoff; on the boundary it clips components that would push the state outside the simplex. Particularly clean mathematical properties but more complex to implement than other dynamics.

§5Payoff Helpers

These functions construct payoff functions and compute the derived quantities that the dynamics use internally. They correspond to Sandholm's Fbar, Fhat, Fhatplus, and PhiF.

Normal-Form Payoff

For a symmetric game with $n \times n$ payoff matrix $A$, the payoff function is $F(\mathbf{x}) = A\mathbf{x}$:

const A = [[3,0],[5,1]];
const F = normalFormPayoff(A);
const payoffs = F([0.6, 0.4]);  // → [1.8, 3.4]

Derived Quantities

averagePayoff(x, F)    // x·F(x), scalar
excessPayoff(x, F)     // F(x) − x·F(x), vector
excessPayoffPlus(x, F) // max(0, F(x) − x·F(x)), vector
projectedPayoff(x, F)  // P·F(x), projected onto TΔ

§6Numerical Integration

The library provides an adaptive Runge–Kutta–Fehlberg (RKF45) integrator that replaces Mathematica's NDSolve. After each step, the state is projected back onto the simplex to correct numerical drift. Adaptive step sizing handles the stiffness that logit dynamics exhibit near the boundary.

const traj = integrate(replicator, F,
  [0.5, 0.3, 0.2],  // initial state
  50,                  // end time
  {
    dt: 0.01,           // initial step size
    tol: 1e-8,          // error tolerance
    adaptive: true,     // adaptive stepping
    outputInterval: 0.1 // sample every 0.1 time units
  }
);

// traj = [{t: 0, x: [0.5, 0.3, 0.2]}, {t: 0.1, x: [...]}, ...]

For velocity fields and quiver plots, use velocityField:

const field = velocityField(smith, F, 25, 3, 0.02);
// → [{x: [...], v: [...]}, ...] on a 25-subdivision grid

§7Lyapunov Functions

For stable games (games with a negative semi-definite payoff Jacobian $DF + DF^\top \preceq 0$, i.e., the class of games where all Nash equilibria are evolutionarily stable), Sandholm defines Lyapunov functions for each major dynamic whose decrease along trajectories proves global convergence.

DynamicLyapunov function $V(\mathbf{x})$Export
Replicator $-\prod_j x_j^{x_j^*}$ (KL divergence) replicatorLyapunov(x*)
BNN $\sum_j [\hat{F}_j]^{+2}$ bnnLyapunov(F)
Smith $\sum_k x_k \sum_j [F_j - F_k]^{+2}$ smithLyapunov(F)
Logit $\eta \log\sum_j e^{F_j/\eta} - \mathbf{x}\cdot F(\mathbf{x}) + \eta H(\mathbf{x})$ logitLyapunov(F, η)
Projection $\|\mathbf{x} - \mathbf{x}^*\|^2$ projectionLyapunov(x*)
const V = bnnLyapunov(F);
for (const { t, x } of trajectory) {
  console.log(t, V(x));  // should decrease in stable games
}

§8Simplex Utilities

Coordinate Conversions

For the 3-simplex, baryToCart maps barycentric coordinates $(x_1, x_2, x_3)$ to Cartesian $(p_x, p_y)$ in an equilateral triangle with vertices $e_1 = (0.5, \tfrac{\sqrt{3}}{2})$ (top), $e_2 = (0, 0)$ (bottom-left), $e_3 = (1, 0)$ (bottom-right).

const [px, py] = baryToCart([0.4, 0.35, 0.25]);
const bary     = cartToBary(px, py); // roundtrip

Grid Generation

simplexGrid(3, 10)         // 66 points on boundary + interior
interiorSimplexGrid(3, 20) // interior only, margin = 0.01

Simplex Projection

After numerical integration steps, project back onto the simplex:

const xCorrected = projectToSimplex(xDrifted);

§9API Reference

Payoff Construction

normalFormPayoff(A) → F

Create a payoff function for a normal-form game with payoff matrix $A$. Returns $F(\mathbf{x}) = A\mathbf{x}$.

A : number[][]
$n \times n$ payoff matrix
returns : function
Payoff function $F: \Delta \to \mathbb{R}^n$
averagePayoff(x, F) → number

Average payoff $\bar{F}(\mathbf{x}) = \mathbf{x} \cdot F(\mathbf{x})$.

excessPayoff(x, F) → number[]

Excess payoff vector $\hat{F}(\mathbf{x}) = F(\mathbf{x}) - \bar{F}(\mathbf{x})$.

excessPayoffPlus(x, F) → number[]

Positive part of excess payoff: $[\hat{F}_j]^{+} = \max(0, \hat{F}_j)$.

projectedPayoff(x, F) → number[]

Payoff projected onto tangent space of simplex.

Dynamics

replicator(x, F) → number[]

Replicator dynamic. $\dot{x}_j = x_j \hat{F}_j(\mathbf{x})$.

maynardSmithReplicator(x, F) → number[]

Maynard Smith replicator. Requires positive average payoff.

bnn(x, F) → number[]

Brown–von Neumann–Nash dynamic.

smith(x, F) → number[]

Smith pairwise comparison dynamic.

logit(η) → function(x, F)

Creates a logit dynamic with noise level $\eta$.

ilogit(η) → function(x, F)

Creates an independent logit dynamic with noise level $\eta$.

bestResponse(η?) → function(x, F)

Best response dynamic (low-temperature logit approximation, default $\eta = 0.001$).

projection(x, F) → number[]

Projection dynamic onto tangent cone.

excessPayoffDynamic(σ̃?) → function(x, F)

General excess payoff dynamic with configurable growth function $\tilde\sigma$. Default: $v \mapsto [\max(0,v)]^2$.

pairwiseComparisonDynamic(ρ?) → function(x, F)

General pairwise comparison dynamic with configurable switch rate $\rho$. Default: $d \mapsto \max(d, 0)$ (Smith).

temperedLogit(η, Q?) → function(x, F)

Tempered logit dynamic with tempering function $Q$.

combineDynamics(dyn1, dyn2, w1, w2) → function(x, F)

Convex combination of two dynamics.

Integration

integrate(dynamic, F, x0, T, opts?) → TrajectoryPoint[]

Integrate a dynamic forward in time using adaptive RKF45.

dynamic : function
Dynamic function $(x, F) \to \dot{x}$
F : function
Payoff function
x0 : number[]
Initial state on the simplex
T : number
End time
opts.dt : number
Initial step size (default 0.01)
opts.tol : number
Error tolerance (default 1e-8)
opts.adaptive : boolean
Adaptive stepping (default true)
opts.outputInterval : number
Output sampling interval (default 0, every step)
opts.maxSteps : number
Maximum steps (default 50000)

Simplex Utilities

baryToCart(x) → [px, py]

Barycentric to Cartesian for the 3-simplex.

cartToBary(px, py) → [x1, x2, x3]

Cartesian to barycentric for the 3-simplex.

simplexGrid(n, k) → number[][]

Uniform grid on the $n$-simplex with $k$ subdivisions.

interiorSimplexGrid(n, k, margin?) → number[][]

Interior grid excluding points within margin of the boundary.

velocityField(dynamic, F, k, n?, margin?) → {x, v}[]

Velocity field on a simplex grid.

projectToSimplex(x) → number[]

Nearest-point projection onto the simplex.

support(x, eps?) → number[]

Indices of strategies with positive mass.

Preset Games

ROCK_PAPER_SCISSORS, COORDINATION_3x3, PRISONERS_DILEMMA

Built-in payoff matrices.

game2x2(a, b, c, d) → number[][]

Construct a $2 \times 2$ payoff matrix $[[a,b],[c,d]]$.