@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.
Files changed (2) hide show
  1. package/README.md +70 -34
  2. 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
- ```bash
7
+ ```sh
8
8
  npm add @sdxc/random
9
9
  ```
10
10
 
11
- The main entry point has no dependencies. `@sdxc/random/schema` exports `RANDOM_STATE_SCHEMA`, a [`remix/data-schema`](https://www.npmjs.com/package/remix) schema; install `remix` to use it.
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
- ### Rolling From A Seed
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 six numbers, in the same order, on every run
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
- ### Accepting Randomness In Your Own API
29
+ ### Accept randomness in your own API
30
30
 
31
- Declare the parameter as `Random` and default it to `systemRandom()`. Production passes nothing, and a test passes a seeded stream instead of stubbing `Math.random`.
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); // crypto-backed
42
+ shouldSample(0.1); // Web Crypto backed
43
43
  shouldSample(0.1, createRandom("sampler-test")); // reproducible
44
44
  ```
45
45
 
46
- ### Saving And Resuming A Stream
46
+ ### Save and resume a stream
47
47
 
48
- `state()` is plain JSON. `restoreRandom` continues from the next draw the original would have taken.
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
- ### Independent Streams From One Seed
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 never shift the values another part sees.
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
- Open 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.
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
- Rebuild a stream from a `state()` snapshot, positioned exactly where the snapshot was taken.
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 short tie-breakers. It has no seed and no state, so it cannot be replayed. Use Web Crypto directly, or a token library, for keys and secrets.
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 the run replays by passing it back to `createRandom`.
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` from `@sdxc/random/schema`
93
+ ### `RANDOM_STATE_SCHEMA: Schema<unknown, RandomState>`
94
94
 
95
- A `remix/data-schema` schema for a `RandomState` read from storage. It checks the seed's type and that each of the four words is an unsigned 32-bit integer.
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
- ### `Random`
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` on non-integer bounds or `max < min`
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 untouched
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
- ### `SeededRandom`
114
+ #### `Seed`
109
115
 
110
- A `Random` with `seed`, `derive(label)` and `state()`.
116
+ `number | string`.
111
117
 
112
- ### `Seed` and `RandomState`
118
+ #### `RandomState`
113
119
 
114
- `Seed` is `number | string`. `RandomState` is `{ seed, words }`, where `words` holds the generator's four unsigned 32-bit words.
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 Run
122
+ ## Pattern: A Reproducible Fuzz Test
117
123
 
118
- Draw a fresh seed per run unless one is supplied, and name the suite after it, so a failure reports the number that replays it.
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
- const SEED = Number(process.env.FUZZ_SEED) || systemSeed();
130
+ import { parseCron } from "./cron.js";
125
131
 
126
- describe(`cron parsing (FUZZ_SEED=${SEED})`, () => {
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(SEED);
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
- `parseCron` is the function under test.
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 each stream's state next to the world it shaped, and validate it on load, since a save file is untrusted input.
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
- const SAVE_SCHEMA = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
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(SAVE_SCHEMA, JSON.parse(text));
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 game as it was, and `load` answers `false` so the caller can say the file was unreadable.
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.6",
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": "b1067eb20fc47ec51ff8c4de47d07dc3f9c00466",
19
+ "gitHead": "5ff758a57e35480a8db7abd24f1d9527b81bfb51",
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },