@sdxc/random 2026.10.6 → 2026.10.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -34
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,31 +4,31 @@ Seeded and system random streams with integer, float, pick and shuffle draws, an
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
```
|
|
7
|
+
```sh
|
|
8
8
|
npm add @sdxc/random
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The main entry point has no dependencies. `@sdxc/random/schema`
|
|
11
|
+
The main entry point has no dependencies. The `@sdxc/random/schema` entry point needs [`remix`](https://www.npmjs.com/package/remix) for `remix/data-schema`.
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
|
-
###
|
|
15
|
+
### Roll from a seed
|
|
16
16
|
|
|
17
17
|
```typescript
|
|
18
18
|
import { createRandom } from "@sdxc/random";
|
|
19
19
|
|
|
20
20
|
let dice = createRandom("battle-42");
|
|
21
21
|
|
|
22
|
-
dice.int(1, 6); // the same
|
|
22
|
+
dice.int(1, 6); // the same values, in the same order, on every run
|
|
23
23
|
dice.float(0.85, 1);
|
|
24
24
|
dice.bool(0.1);
|
|
25
25
|
dice.pick(["rock", "paper", "scissors"]);
|
|
26
26
|
dice.shuffle([1, 2, 3, 4, 5]);
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
###
|
|
29
|
+
### Accept randomness in your own API
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Type the parameter as `Random` and default it to `systemRandom()`. Production passes nothing; a test passes a seeded stream instead of stubbing `Math.random`.
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
34
|
import type { Random } from "@sdxc/random";
|
|
@@ -39,13 +39,13 @@ function shouldSample(rate: number, random: Random = systemRandom()): boolean {
|
|
|
39
39
|
return random.bool(rate);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
shouldSample(0.1); //
|
|
42
|
+
shouldSample(0.1); // Web Crypto backed
|
|
43
43
|
shouldSample(0.1, createRandom("sampler-test")); // reproducible
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Save and resume a stream
|
|
47
47
|
|
|
48
|
-
`state()` is plain JSON
|
|
48
|
+
`state()` is plain JSON, and `restoreRandom` continues with the draw the original would have taken next.
|
|
49
49
|
|
|
50
50
|
```typescript
|
|
51
51
|
import { createRandom, restoreRandom } from "@sdxc/random";
|
|
@@ -59,9 +59,9 @@ let resumed = restoreRandom(JSON.parse(saved));
|
|
|
59
59
|
resumed.int(1, 100) === encounters.int(1, 100); // true
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### Split one seed into independent streams
|
|
63
63
|
|
|
64
|
-
`derive(label)` opens a stream seeded from the parent's seed and the label, so extra draws in one part of a program
|
|
64
|
+
`derive(label)` opens a stream seeded from the parent's seed and the label, so extra draws in one part of a program leave the values of another part unchanged.
|
|
65
65
|
|
|
66
66
|
```typescript
|
|
67
67
|
import { createRandom, systemSeed } from "@sdxc/random";
|
|
@@ -76,56 +76,64 @@ let loot = session.derive("loot");
|
|
|
76
76
|
|
|
77
77
|
### `createRandom(seed: Seed): SeededRandom`
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
Opens a stream on a seed. The same seed produces the same values in the same order on any machine; `42` and `"42"` name the same stream.
|
|
80
80
|
|
|
81
81
|
### `restoreRandom(state: RandomState): SeededRandom`
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Rebuilds a stream from a `state()` snapshot, positioned exactly where the snapshot was taken.
|
|
84
84
|
|
|
85
85
|
### `systemRandom(): Random`
|
|
86
86
|
|
|
87
|
-
An unseeded stream drawing from `crypto.getRandomValues`, for jitter, sampling and
|
|
87
|
+
An unseeded stream drawing from `crypto.getRandomValues`, for jitter, sampling and tie-breakers. It has no seed or state, so it never replays. Generate keys and secrets with Web Crypto directly.
|
|
88
88
|
|
|
89
89
|
### `systemSeed(): number`
|
|
90
90
|
|
|
91
|
-
A fresh 32-bit seed from `crypto.getRandomValues`. Log it, and
|
|
91
|
+
A fresh 32-bit seed from `crypto.getRandomValues`. Log it, and passing it back to `createRandom` replays the run.
|
|
92
92
|
|
|
93
|
-
### `RANDOM_STATE_SCHEMA
|
|
93
|
+
### `RANDOM_STATE_SCHEMA: Schema<unknown, RandomState>`
|
|
94
94
|
|
|
95
|
-
A `remix/data-schema` schema for a `RandomState` read from storage
|
|
95
|
+
From `@sdxc/random/schema`. A `remix/data-schema` schema for a `RandomState` read from storage: the seed is a string or number, and each of the four words is an unsigned 32-bit integer.
|
|
96
96
|
|
|
97
|
-
###
|
|
97
|
+
### Types
|
|
98
|
+
|
|
99
|
+
#### `Random`
|
|
98
100
|
|
|
99
101
|
The draws every stream offers:
|
|
100
102
|
|
|
101
103
|
- `next()`: the raw draw, in `[0, 1)`
|
|
102
|
-
- `int(min, max)`: an integer with both ends included; throws `RangeError`
|
|
104
|
+
- `int(min, max)`: an integer with both ends included; throws `RangeError` when a bound is not a safe integer or `max < min`
|
|
103
105
|
- `float(min = 0, max = 1)`: a number in `[min, max)`
|
|
104
106
|
- `bool(chance = 0.5)`: `true` with probability `chance`
|
|
105
107
|
- `pick(items)`: one element, each equally likely; throws `RangeError` on an empty list
|
|
106
|
-
- `shuffle(items)`: a shuffled copy, leaving the input
|
|
108
|
+
- `shuffle(items)`: a shuffled copy, leaving the input in its order
|
|
109
|
+
|
|
110
|
+
#### `SeededRandom`
|
|
111
|
+
|
|
112
|
+
A `Random` plus `seed`, `derive(label: string): SeededRandom` and `state(): RandomState`.
|
|
107
113
|
|
|
108
|
-
|
|
114
|
+
#### `Seed`
|
|
109
115
|
|
|
110
|
-
|
|
116
|
+
`number | string`.
|
|
111
117
|
|
|
112
|
-
|
|
118
|
+
#### `RandomState`
|
|
113
119
|
|
|
114
|
-
`Seed
|
|
120
|
+
`{ seed: Seed; words: [number, number, number, number] }`, the seed and the generator's four unsigned 32-bit words.
|
|
115
121
|
|
|
116
|
-
## Pattern: A Reproducible Fuzz
|
|
122
|
+
## Pattern: A Reproducible Fuzz Test
|
|
117
123
|
|
|
118
|
-
Draw a fresh seed per run unless one is supplied, and
|
|
124
|
+
Draw a fresh seed per run unless one is supplied, and put it in the suite name, so a failure reports the number that replays it.
|
|
119
125
|
|
|
120
126
|
```typescript
|
|
121
127
|
import { createRandom, systemSeed } from "@sdxc/random";
|
|
122
128
|
import { describe, expect, test } from "vitest";
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
import { parseCron } from "./cron.js";
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
let seed = Number(process.env.FUZZ_SEED) || systemSeed();
|
|
133
|
+
|
|
134
|
+
describe(`cron parsing (FUZZ_SEED=${seed})`, () => {
|
|
127
135
|
test("every generated expression parses", () => {
|
|
128
|
-
let random = createRandom(
|
|
136
|
+
let random = createRandom(seed);
|
|
129
137
|
|
|
130
138
|
for (let index = 0; index < 1000; index++) {
|
|
131
139
|
let minute = random.int(0, 59);
|
|
@@ -137,11 +145,39 @@ describe(`cron parsing (FUZZ_SEED=${SEED})`, () => {
|
|
|
137
145
|
});
|
|
138
146
|
```
|
|
139
147
|
|
|
140
|
-
|
|
148
|
+
## Pattern: Stable Procedural Generation
|
|
149
|
+
|
|
150
|
+
Give each feature of a generated world its own derived stream. Adding a draw to the terrain generator later keeps every existing world's loot exactly as it was.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import type { Random } from "@sdxc/random";
|
|
154
|
+
|
|
155
|
+
import { createRandom } from "@sdxc/random";
|
|
156
|
+
|
|
157
|
+
function terrain(random: Random): string[] {
|
|
158
|
+
return Array.from({ length: 8 }, () => random.pick(["plains", "forest", "hills", "lake"]));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function loot(random: Random): { gold: number; items: string[] } {
|
|
162
|
+
let gold = random.int(10, 100);
|
|
163
|
+
let items = random.shuffle(["sword", "shield", "potion", "map"]).slice(0, random.int(1, 3));
|
|
164
|
+
return { gold, items };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function generateWorld(seed: string) {
|
|
168
|
+
let world = createRandom(seed);
|
|
169
|
+
return {
|
|
170
|
+
terrain: terrain(world.derive("terrain")),
|
|
171
|
+
loot: loot(world.derive("loot")),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
generateWorld("world-7"); // identical output for "world-7" on every machine
|
|
176
|
+
```
|
|
141
177
|
|
|
142
178
|
## Pattern: Persisting A Simulation
|
|
143
179
|
|
|
144
|
-
Store
|
|
180
|
+
Store the stream's state next to the data it shaped, and validate it on load, since a save file is untrusted input.
|
|
145
181
|
|
|
146
182
|
```typescript
|
|
147
183
|
import * as s from "remix/data-schema";
|
|
@@ -149,7 +185,7 @@ import * as s from "remix/data-schema";
|
|
|
149
185
|
import { createRandom, restoreRandom } from "@sdxc/random";
|
|
150
186
|
import { RANDOM_STATE_SCHEMA } from "@sdxc/random/schema";
|
|
151
187
|
|
|
152
|
-
|
|
188
|
+
let saveSchema = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
|
|
153
189
|
|
|
154
190
|
let random = createRandom("campaign");
|
|
155
191
|
let turn = 0;
|
|
@@ -159,7 +195,7 @@ function save(): string {
|
|
|
159
195
|
}
|
|
160
196
|
|
|
161
197
|
function load(text: string): boolean {
|
|
162
|
-
let parsed = s.parseSafe(
|
|
198
|
+
let parsed = s.parseSafe(saveSchema, JSON.parse(text));
|
|
163
199
|
if (!parsed.success) return false;
|
|
164
200
|
|
|
165
201
|
turn = parsed.value.turn;
|
|
@@ -168,7 +204,7 @@ function load(text: string): boolean {
|
|
|
168
204
|
}
|
|
169
205
|
```
|
|
170
206
|
|
|
171
|
-
A save that fails the schema leaves the running
|
|
207
|
+
A save that fails the schema leaves the running simulation untouched, and `load` answers `false`.
|
|
172
208
|
|
|
173
209
|
## Versioning
|
|
174
210
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/random",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.13",
|
|
4
4
|
"description": "Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"optional": true
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
-
"gitHead": "
|
|
19
|
+
"gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
|
|
20
20
|
"publishConfig": {
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|