npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@its-not-rocket-science/ananke

v0.1.16

Published

Deterministic lockstep-friendly SI-units RPG/physics core (fixed-point TS)

Readme

Ananke — Programmer's Guide

CI

Package: @its-not-rocket-science/ananke Full project overview: docs/project-overview.md


What is Ananke?

Ananke is a deterministic, physics-grounded simulation engine for characters, combat, and survivability. It models entities using real physical quantities — mass in kg, force in newtons, energy in joules — rather than abstract hit points or dice rolls.

Same seed + same inputs → identical results, every time. No floating-point drift. Suitable for lockstep multiplayer, reproducible research, and offline AI training.


Installation

npm install @its-not-rocket-science/ananke

Requires Node ≥ 18. ESM-only. TypeScript declarations included — no @types/ package needed. Zero runtime dependencies.

Versioning: pin to a specific version in production. The 0.x series may include minor-version breaking changes to Tier 2 (experimental) APIs; Tier 1 (Stable) APIs follow full semver. See STABLE_API.md for the tier breakdown and docs/versioning.md for the upgrade policy.


Core concepts

Fixed-point arithmetic

All simulation values use Q — a fixed-point integer where SCALE.Q = 16384 represents 1.0. Never use raw number for simulation values; always use q() to construct them.

import { q, SCALE } from "@its-not-rocket-science/ananke";

const half   = q(0.50);   // 8192  — 50%
const full   = q(1.00);   // 16384 — 100%
const eighty = q(0.80);   // 13107 — 80%

// SI unit scales
SCALE.m;    // 1000  — 1 metre in fixed-point units
SCALE.kg;   // 1000  — 1 kilogram
SCALE.mps;  // 1000  — 1 m/s
SCALE.J;    // 1     — 1 joule (energy is stored at 1:1)

You will see values like position_m: { x: 3000, y: 0, z: 0 } — that is 3 metres on the x-axis (3000 / SCALE.m = 3). The _m, _kg, _J, _s suffixes on field names tell you the unit.

The Entity

An Entity is any simulated object. Required fields at creation:

import type { Entity } from "@its-not-rocket-science/ananke";

| Field | Type | Meaning | |---|---|---| | id | number | Unique integer; used as RNG salt | | teamId | number | Entities attack those on different teams | | position_m | Vec3 | World-space position in fixed-point metres | | attributes | IndividualAttributes | Physical stats (force, power, mass…) | | energy | { current_J, max_J } | Stamina pool in joules | | injury | InjuryState | Per-region damage accumulation | | condition | ConditionSnapshot | Shock, fear, fatigue | | loadout | { items: Item[] } | Equipped weapons and armour |

Use a factory instead of constructing these manually — see Quick starts below.

The simulation loop

import { mkWorld, stepWorld } from "@its-not-rocket-science/ananke";

const world = mkWorld(seed, entities);     // create world with deterministic seed

for (let tick = 0; tick < 2000; tick++) {
  const commands = buildCommands(world);   // your AI / player input
  stepWorld(world, commands, ctx);         // mutates world in-place
}

stepWorld is the only function that mutates state. Everything else is pure computation. Call it at 20 Hz for real-time simulation; 1 Hz or lower for campaign-scale time.


Quick start A — Melee combat

Two fighters, one fight, three seeds:

import {
  mkWorld, stepWorld, generateIndividual, q,
  SCALE, STARTER_WEAPONS, STARTER_ARMOUR,
  buildAICommands, buildWorldIndex, buildSpatialIndex,
  AI_PRESETS,
} from "@its-not-rocket-science/ananke";
import type { Q } from "@its-not-rocket-science/ananke";

const LONGSWORD = STARTER_WEAPONS[2]!;
const LEATHER   = STARTER_ARMOUR[0]!;

function makeEntity(id: number, teamId: number, x_m: number) {
  const e = generateIndividual("KNIGHT_INFANTRY", id, teamId);
  e.position_m = { x: x_m * SCALE.m, y: 0, z: 0 };
  e.loadout    = { items: [LONGSWORD, LEATHER] };
  return e;
}

const policy = AI_PRESETS["lineInfantry"]!;

for (const seed of [1, 42, 99]) {
  const a = makeEntity(1, 1, -2);
  const b = makeEntity(2, 2, +2);
  const world = mkWorld(seed, [a, b]);
  const ctx   = { tractionCoeff: q(0.85) as Q };

  let tick = 0;
  while (tick < 2000 && !a.injury.dead && !b.injury.dead) {
    tick++;
    const idx  = buildWorldIndex(world);
    const spat = buildSpatialIndex(world, 40_000);
    const cmds = buildAICommands(world, idx, spat, () => policy);
    stepWorld(world, cmds, ctx);
  }

  const winner = a.injury.dead ? "B" : b.injury.dead ? "A" : "draw";
  console.log(`seed=${seed}  winner=${winner}  ticks=${tick}`);
}

Reading injury state

for (const [region, inj] of Object.entries(entity.injury.regions)) {
  const pct = (inj.surfaceDamage / SCALE.Q * 100).toFixed(0);
  if (inj.surfaceDamage > 0)
    console.log(`  ${region}: ${pct}% surface damage${inj.infected ? " [infected]" : ""}`);
}
console.log(`  dead:  ${entity.injury.dead}`);
console.log(`  shock: ${(entity.condition.shockQ / SCALE.Q * 100).toFixed(0)}%`);

Using the narrative layer

import {
  CollectingTrace, renderChronicle,
} from "@its-not-rocket-science/ananke";

const trace = new CollectingTrace();
stepWorld(world, commands, { ...ctx, trace });

const log = renderChronicle(trace.events, world.entities, { verbosity: "normal" });
console.log(log);
// → "Knight strikes Brawler in the torso for 340 J. Brawler staggers."

Quick start B — Campaign and world simulation

Advance two polities through 90 days with tech diffusion and emotional contagion:

import {
  createPolityRegistry, stepPolityDay,
  applyEmotionalContagion, stepTechDiffusion,
  createEmotionalWave, FEAR_WAVE, q, SCALE,
} from "@its-not-rocket-science/ananke";

const WORLD_SEED = 1;

const registry = createPolityRegistry([
  { id: 1, name: "Ironhold", population: 50_000, techEra: 2, moraleQ: q(0.70) /* ... */ },
  { id: 2, name: "Ashfeld",  population: 30_000, techEra: 1, moraleQ: q(0.55) /* ... */ },
]);

const pairs = [{ polityA: 1, polityB: 2, routeQuality_Q: q(0.60), atWar: false /* ... */ }];

for (let day = 1; day <= 90; day++) {
  stepPolityDay(registry, WORLD_SEED, day);
  stepTechDiffusion(registry, pairs, WORLD_SEED, day);
  applyEmotionalContagion(registry, [createEmotionalWave(FEAR_WAVE, 1)], pairs);
}

for (const p of registry.polities) {
  console.log(`${p.name}: pop=${p.population}  era=${p.techEra}  morale=${(p.moraleQ / SCALE.Q).toFixed(2)}`);
}

Quick start C — Species and character generation

Generate individuals from a body-plan archetype, apply aging, and describe them:

import {
  generateIndividual, applyAgingToAttributes,
  describeCharacter, formatCharacterSheet,
} from "@its-not-rocket-science/ananke";

// Generate a 45-year-old knight
const base = generateIndividual("KNIGHT_INFANTRY", 1, 1);
const aged = applyAgingToAttributes(base.attributes, 45);

console.log(formatCharacterSheet({ ...base, attributes: aged }));
// → Strength: 1840 N  [above average]
//   Reaction: 0.21 s  [average]
//   ...

// Fantasy species
const elf = generateIndividual("ELF_ARCHER", 2, 2);
console.log(describeCharacter(elf));

Available built-in archetypes: KNIGHT_INFANTRY, PRO_BOXER, GRECO_WRESTLER, AMATEUR_BOXER, LARGE_PACIFIC_OCTOPUS, and all species defined in src/species.ts — humans, elves, dwarves, orcs, dragons, Vulcans, Klingons, and more.


The command system

stepWorld takes a CommandMap — a Map<entityId, EntityCommand>. You build it manually, from your AI layer, or from the built-in AI system:

import type { EntityCommand } from "@its-not-rocket-science/ananke";

// Attack
const commands = new Map<number, EntityCommand>([
  [entityId, { kind: "attack", targetId: opponentId, weapon: LONGSWORD }],
]);

// Move to a position
commands.set(entityId, {
  kind:        "move",
  destination: { x: 5 * SCALE.m, y: 0, z: 0 },
});

// Treat a wounded ally
commands.set(medicId, {
  kind:     "treat",
  targetId: woundedId,
  schedule: { care: "field_surgery", equipmentTier: 2 },
});

Valid kind values: "attack", "move", "grapple", "treat", "use_capability", "signal", "idle".


Determinism

Ananke guarantees that mkWorld(seed, entities) followed by identical commands produces identical WorldState at every tick, regardless of platform, JS engine, or execution time.

Rules to preserve determinism in your host:

  1. Never use Math.random() — use makeRng(eventSeed(...)) from the package instead
  2. Iterate world.entities in insertion order (it is a stable array, not a Map)
  3. Keep entity id values stable across ticks — IDs are used as RNG salts
  4. Do not rely on wall-clock time inside the simulation loop
import { makeRng, eventSeed } from "@its-not-rocket-science/ananke";

// Deterministic RNG inside your AI or event code:
const rng  = makeRng(eventSeed(world.seed, world.tick, entityId, 0, 42));
const roll = rng();  // float in [0, 1) — deterministic from inputs

Replay and serialisation

import {
  ReplayRecorder, serializeReplay, deserializeReplay, replayTo,
} from "@its-not-rocket-science/ananke";

// Record
const recorder = new ReplayRecorder();
for (let tick = 0; tick < N; tick++) {
  const cmds = buildCommands(world);
  recorder.record(tick, cmds);
  stepWorld(world, cmds, ctx);
}
const json = serializeReplay(recorder.replay);  // stable JSON string

// Replay to any tick
const replay = deserializeReplay(json);
const state  = replayTo(replay, initialWorld, targetTick, ctx);

3D renderer bridge

Extract per-segment pose data for driving a humanoid rig at renderer frame rate:

import {
  extractRigSnapshots, deriveAnimationHints, BridgeEngine,
} from "@its-not-rocket-science/ananke";

// Per-tick: get bone transforms
const snapshots = extractRigSnapshots(world.entities, bodyPlan);
// snapshots[entityId] → RigSnapshot { segments: Map<segmentId, { position_m, rotation }> }

// Per-tick: get animation state machine hints
const hints = deriveAnimationHints(entity);
// hints → { idle, walk, run, attacking, prone, unconscious, dead, shockQ, fearQ, ... }

// Or use BridgeEngine for double-buffered interpolation at renderer frame rate:
const bridge = new BridgeEngine(config);
bridge.writeSimFrame(world.tick, world.entities);
const interp = bridge.readInterpolated(rendererTimestamp);

See docs/bridge-contract.md for the full double-buffer protocol and AnimationHints field-by-field contract.


API stability tiers

| Tier | Guarantee | Examples | |------|-----------|---------| | Tier 1 — Stable | Breaking changes require major semver bump + migration guide | stepWorld, generateIndividual, Entity, q, SCALE, bridge module | | Tier 2 — Experimental | May change in minor versions; CHANGELOG will note it | Campaign, polity, dialogue, faction, quest subsystems | | Tier 3 — Internal | No stability guarantee; may change at any time | makeRng, eventSeed, kernel tuning constants, mkHumanoidEntity |

Full tier table: STABLE_API.md


TypeScript

The package ships full .d.ts declarations. Key types to know:

import type {
  Entity,               // the simulated object
  WorldState,           // world.entities + world.tick + world.seed
  KernelContext,        // tractionCoeff, weather, etc. — passed to stepWorld
  EntityCommand,        // what an entity does this tick
  IndividualAttributes, // physical stats (SI units)
  InjuryState,          // per-region damage
  ConditionSnapshot,    // shock, fear, fatigue
  Q,                    // fixed-point number alias (just `number` at runtime)
  Vec3,                 // { x, y, z } in fixed-point metres
} from "@its-not-rocket-science/ananke";

Q is a nominal alias for number — it carries no runtime overhead, but the q() constructor and SCALE constants make the intent clear in every formula.


Performance guidance

| Scenario | Recommended tick rate | Practical entity cap | |---|---|---| | Duel / 1v1 | 20 Hz | Unlimited | | Skirmish (squads) | 20 Hz | ~300 | | Battle (formations) | 10 Hz | ~500 | | Siege / campaign | 1 Hz | ~1 000 | | World simulation | 0.01 Hz (once/day) | ~10 000 |

Enable buildSpatialIndex when entities exceed ~50 and distances matter. Disable expensive subsystems (disease O(n²) spread, thermoregulation) at high entity counts unless required.

Full benchmark methodology and operational guide: docs/performance.md


Validation and trust

Ananke's outputs are validated against historical and experimental sources:

  • Isolated sub-system validation — compares physical constants against sport-science and biomechanics datasets: npm run run:validation
  • Emergent validation — four historical combat scenarios (du Picq, Keegan, Lanchester, Raudzens) across 100 seeds each: npm run run:emergent-validation
  • Pinned baseline — committed result summaries that CI guards against regression: docs/emergent-validation-report.md

Further reading

| Document | What's in it | |---|---| | docs/host-contract.md | Stable integration surface — everything needed to embed Ananke without reading src/ | | docs/integration-primer.md | Data-flow diagrams, type glossary, gotchas | | docs/bridge-contract.md | 3D renderer bridge protocol (AnimationHints, GrapplePoseConstraint) | | STABLE_API.md | Full tier table for every export | | docs/versioning.md | Semver policy, breaking-change tiers, upgrade cadence | | docs/performance.md | Benchmark results, operational guide, entity caps | | docs/emergent-validation-report.md | Historical scenario validation report | | docs/project-overview.md | Full project overview — implementation status, entity model reference, design principles, architecture |