Floating-point math breaks determinism in multiplayer games and replay systems. A tiny CPU rounding difference can desync entire game states. I needed math that returns identical results on every machine, every time.
So I built @shaisrc/fixed-point - a zero-dependency fixed-point library for deterministic simulation in TypeScript.
π¦ The Library
- Repo: github.com/ShaiSrc/fixed-point
-
NPM:
npm install @shaisrc/fixed-point
π How it works
Unlike standard number math, this library treats values as integers scaled by a fixed factor (default Q16.16). It guarantees that calculations result in the exact same bit-pattern everywhere.
What makes this library special is the hybrid storage engine:
- If you need standard 32-bit precision, it uses native
numberfor speed. - If you need high precision (e.g., 64-bit), it automatically swaps to
BigIntarithmetic.
Quick Start
Here is how you perform deterministic operations:
import { fp } from '@shaisrc/fixed-point';
// 1. Create fixed-point numbers from floats or integers
const position = fp.fromFloat(10.5); // Stored as integer: 688128 (10.5 * 2^16)
const velocity = fp.fromInt(2); // Stored as integer: 131072 (2 * 2^16)
// 2. Perform math (all operations happen on scaled integers)
const newPos = fp.add(position, velocity);
// 3. Convert back only when rendering
console.log(fp.toFloat(newPos)); // 12.5
// 4. Deterministic Trig (uses a LUT, no Math.sin calls!)
const angle = fp.fromFloat(Math.PI / 2);
const sine = fp.sin(angle); // Returns fixed-point 1.0 representation
βοΈ Under the Hood
Configurable Precision
By default, it uses Q16.16 (16 bits for the integer, 16 bits for the fraction). But you aren't stuck there. You can configure your own instance:
import { createFixedPoint } from '@shaisrc/fixed-point';
// Need massive precision? Go 64-bit with BigInt backend.
const bigMath = createFixedPoint({
totalBits: 64,
fractionBits: 32,
useBigInt: true
});
const hugeVal = bigMath.fromString("12345.67890123");
Performance & Features
- Bitwise Optimized: Shifts and masks are used wherever possible.
-
Lookup Tables (LUT):
sinandcosuse pre-calculated tables to ensure different browser engines don't return slightly different float results. -
Newton Iteration:
sqrtis implemented with integer-based Newton iteration. - Type Safety: Full TypeScript support prevents you from accidentally mixing up raw numbers and fixed-point values.
π Performance
Here's how it compares to native operations (5M iterations):
| Benchmark | Min (ms) | Avg (ms) |
|---|---|---|
| native add/mul | 6.34 | 6.70 |
| fixed-point add/mul (number) | 116.65 | 120.24 |
| fixed-point add/mul (bigint) | 382.35 | 389.47 |
| native sin | 190.57 | 197.90 |
| fixed-point sin (LUT, number) | 62.75 | 64.75 |
| fixed-point sin (LUT, bigint) | 491.26 | 501.97 |
Key takeaway: Basic math is slower than native (you're trading speed for determinism), but the LUT-based trig functions actually beat Math.sin by ~3x when using the number backend.
Why I Open-Sourced This
This was built for my deterministic game engine's lockstep networking. It works-so I'm sharing it. If you're building networked games, replay systems, or physics sims that need bitwise-identical math, this might save you weeks of debugging float drift.
Feedback on edge cases and performance welcome.
Try it out
npm install @shaisrc/fixed-point
π Check it out on GitHub: https://github.com/ShaiSrc/fixed-point
Found a bug? Have a use case I didn't think of? Open an issue or PR.
Top comments (0)