@sdxc/random 0.0.0-pre.1 → 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 +87 -39
- package/package.json +11 -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,64 +76,108 @@ 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";
|
|
128
|
+
import { describe, expect, test } from "vitest";
|
|
129
|
+
|
|
130
|
+
import { parseCron } from "./cron.js";
|
|
122
131
|
|
|
123
132
|
let seed = Number(process.env.FUZZ_SEED) || systemSeed();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
|
|
134
|
+
describe(`cron parsing (FUZZ_SEED=${seed})`, () => {
|
|
135
|
+
test("every generated expression parses", () => {
|
|
136
|
+
let random = createRandom(seed);
|
|
137
|
+
|
|
138
|
+
for (let index = 0; index < 1000; index++) {
|
|
139
|
+
let minute = random.int(0, 59);
|
|
140
|
+
let hour = random.int(0, 23);
|
|
141
|
+
let weekday = random.pick(["MON", "TUE", "WED", "THU", "FRI"]);
|
|
142
|
+
expect(parseCron(`${minute} ${hour} * * ${weekday}`).success).toBe(true);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
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
|
|
132
176
|
```
|
|
133
177
|
|
|
134
178
|
## Pattern: Persisting A Simulation
|
|
135
179
|
|
|
136
|
-
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.
|
|
137
181
|
|
|
138
182
|
```typescript
|
|
139
183
|
import * as s from "remix/data-schema";
|
|
@@ -141,7 +185,7 @@ import * as s from "remix/data-schema";
|
|
|
141
185
|
import { createRandom, restoreRandom } from "@sdxc/random";
|
|
142
186
|
import { RANDOM_STATE_SCHEMA } from "@sdxc/random/schema";
|
|
143
187
|
|
|
144
|
-
|
|
188
|
+
let saveSchema = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
|
|
145
189
|
|
|
146
190
|
let random = createRandom("campaign");
|
|
147
191
|
let turn = 0;
|
|
@@ -150,14 +194,18 @@ function save(): string {
|
|
|
150
194
|
return JSON.stringify({ turn, random: random.state() });
|
|
151
195
|
}
|
|
152
196
|
|
|
153
|
-
function load(text: string) {
|
|
154
|
-
let parsed = s.parseSafe(
|
|
155
|
-
if (!parsed.success)
|
|
197
|
+
function load(text: string): boolean {
|
|
198
|
+
let parsed = s.parseSafe(saveSchema, JSON.parse(text));
|
|
199
|
+
if (!parsed.success) return false;
|
|
200
|
+
|
|
156
201
|
turn = parsed.value.turn;
|
|
157
202
|
random = restoreRandom(parsed.value.random);
|
|
203
|
+
return true;
|
|
158
204
|
}
|
|
159
205
|
```
|
|
160
206
|
|
|
207
|
+
A save that fails the schema leaves the running simulation untouched, and `load` answers `false`.
|
|
208
|
+
|
|
161
209
|
## Versioning
|
|
162
210
|
|
|
163
211
|
Releases are dated rather than semantic. A version is the UTC date it was published, written `YYYY.M.D`, so `2026.9.4` is the release from 4 September 2026. At most one release goes out per day.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdxc/random",
|
|
3
|
-
"version": "
|
|
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
|
},
|
|
@@ -24,5 +24,14 @@
|
|
|
24
24
|
"type": "git",
|
|
25
25
|
"url": "git+https://github.com/sergiodxa/monorepo.git",
|
|
26
26
|
"directory": "packages/random"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://sdxc.sergiodxa.com/api/random",
|
|
29
|
+
"author": {
|
|
30
|
+
"name": "Sergio Xalambrí",
|
|
31
|
+
"url": "https://sergiodxa.com"
|
|
32
|
+
},
|
|
33
|
+
"funding": {
|
|
34
|
+
"type": "github",
|
|
35
|
+
"url": "https://github.com/sponsors/sergiodxa"
|
|
27
36
|
}
|
|
28
37
|
}
|