@sdxc/random 0.0.0-pre.1
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/LICENSE.md +21 -0
- package/README.md +185 -0
- package/dist/draws.d.ts +40 -0
- package/dist/draws.js +50 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +2 -0
- package/dist/schema.d.ts +17 -0
- package/dist/schema.js +20 -0
- package/dist/seeded.d.ts +52 -0
- package/dist/seeded.js +80 -0
- package/dist/system.d.ts +25 -0
- package/dist/system.js +31 -0
- package/package.json +28 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sergio Xalambrí
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# @sdxc/random
|
|
2
|
+
|
|
3
|
+
Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm add @sdxc/random
|
|
9
|
+
```
|
|
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.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
### Rolling From A Seed
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { createRandom } from "@sdxc/random";
|
|
19
|
+
|
|
20
|
+
let dice = createRandom("battle-42");
|
|
21
|
+
|
|
22
|
+
dice.int(1, 6); // the same six numbers, in the same order, on every run
|
|
23
|
+
dice.float(0.85, 1);
|
|
24
|
+
dice.bool(0.1);
|
|
25
|
+
dice.pick(["rock", "paper", "scissors"]);
|
|
26
|
+
dice.shuffle([1, 2, 3, 4, 5]);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Accepting Randomness In Your Own API
|
|
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`.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import type { Random } from "@sdxc/random";
|
|
35
|
+
|
|
36
|
+
import { createRandom, systemRandom } from "@sdxc/random";
|
|
37
|
+
|
|
38
|
+
function shouldSample(rate: number, random: Random = systemRandom()): boolean {
|
|
39
|
+
return random.bool(rate);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
shouldSample(0.1); // crypto-backed
|
|
43
|
+
shouldSample(0.1, createRandom("sampler-test")); // reproducible
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Saving And Resuming A Stream
|
|
47
|
+
|
|
48
|
+
`state()` is plain JSON. `restoreRandom` continues from the next draw the original would have taken.
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createRandom, restoreRandom } from "@sdxc/random";
|
|
52
|
+
|
|
53
|
+
let encounters = createRandom("world-7");
|
|
54
|
+
encounters.int(1, 100);
|
|
55
|
+
|
|
56
|
+
let saved = JSON.stringify(encounters.state());
|
|
57
|
+
let resumed = restoreRandom(JSON.parse(saved));
|
|
58
|
+
|
|
59
|
+
resumed.int(1, 100) === encounters.int(1, 100); // true
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Independent Streams From One Seed
|
|
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.
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
import { createRandom, systemSeed } from "@sdxc/random";
|
|
68
|
+
|
|
69
|
+
let session = createRandom(systemSeed());
|
|
70
|
+
|
|
71
|
+
let battle = session.derive("battle");
|
|
72
|
+
let loot = session.derive("loot");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API
|
|
76
|
+
|
|
77
|
+
### `createRandom(seed: Seed): SeededRandom`
|
|
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.
|
|
80
|
+
|
|
81
|
+
### `restoreRandom(state: RandomState): SeededRandom`
|
|
82
|
+
|
|
83
|
+
Rebuild a stream from a `state()` snapshot, positioned exactly where the snapshot was taken.
|
|
84
|
+
|
|
85
|
+
### `systemRandom(): Random`
|
|
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.
|
|
88
|
+
|
|
89
|
+
### `systemSeed(): number`
|
|
90
|
+
|
|
91
|
+
A fresh 32-bit seed from `crypto.getRandomValues`. Log it, and the run replays by passing it back to `createRandom`.
|
|
92
|
+
|
|
93
|
+
### `RANDOM_STATE_SCHEMA` from `@sdxc/random/schema`
|
|
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.
|
|
96
|
+
|
|
97
|
+
### `Random`
|
|
98
|
+
|
|
99
|
+
The draws every stream offers:
|
|
100
|
+
|
|
101
|
+
- `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`
|
|
103
|
+
- `float(min = 0, max = 1)`: a number in `[min, max)`
|
|
104
|
+
- `bool(chance = 0.5)`: `true` with probability `chance`
|
|
105
|
+
- `pick(items)`: one element, each equally likely; throws `RangeError` on an empty list
|
|
106
|
+
- `shuffle(items)`: a shuffled copy, leaving the input untouched
|
|
107
|
+
|
|
108
|
+
### `SeededRandom`
|
|
109
|
+
|
|
110
|
+
A `Random` with `seed`, `derive(label)` and `state()`.
|
|
111
|
+
|
|
112
|
+
### `Seed` and `RandomState`
|
|
113
|
+
|
|
114
|
+
`Seed` is `number | string`. `RandomState` is `{ seed, words }`, where `words` holds the generator's four unsigned 32-bit words.
|
|
115
|
+
|
|
116
|
+
## Pattern: A Reproducible Fuzz Run
|
|
117
|
+
|
|
118
|
+
Draw a fresh seed per run unless one is supplied, and print it, so a failing run replays from that number.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { createRandom, systemSeed } from "@sdxc/random";
|
|
122
|
+
|
|
123
|
+
let seed = Number(process.env.FUZZ_SEED) || systemSeed();
|
|
124
|
+
console.info(`fuzz seed: ${seed}`);
|
|
125
|
+
|
|
126
|
+
let random = createRandom(seed);
|
|
127
|
+
let cases = Array.from({ length: 1000 }, () => ({
|
|
128
|
+
minute: random.int(0, 59),
|
|
129
|
+
hour: random.int(0, 23),
|
|
130
|
+
weekday: random.pick(["MON", "TUE", "WED", "THU", "FRI"]),
|
|
131
|
+
}));
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Pattern: Persisting A Simulation
|
|
135
|
+
|
|
136
|
+
Store each stream's state next to the world it shaped, and validate it on load, since a save file is untrusted input.
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
import * as s from "remix/data-schema";
|
|
140
|
+
|
|
141
|
+
import { createRandom, restoreRandom } from "@sdxc/random";
|
|
142
|
+
import { RANDOM_STATE_SCHEMA } from "@sdxc/random/schema";
|
|
143
|
+
|
|
144
|
+
const SAVE_SCHEMA = s.object({ turn: s.number(), random: RANDOM_STATE_SCHEMA });
|
|
145
|
+
|
|
146
|
+
let random = createRandom("campaign");
|
|
147
|
+
let turn = 0;
|
|
148
|
+
|
|
149
|
+
function save(): string {
|
|
150
|
+
return JSON.stringify({ turn, random: random.state() });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function load(text: string) {
|
|
154
|
+
let parsed = s.parseSafe(SAVE_SCHEMA, JSON.parse(text));
|
|
155
|
+
if (!parsed.success) throw new Error("Corrupt save file");
|
|
156
|
+
turn = parsed.value.turn;
|
|
157
|
+
random = restoreRandom(parsed.value.random);
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Versioning
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
Those numbers say when, not what: a later date means a later release and carries no compatibility promise. Any release may change or remove an export.
|
|
166
|
+
|
|
167
|
+
Depend on one exact date, and move it when you are ready to take the change:
|
|
168
|
+
|
|
169
|
+
```json
|
|
170
|
+
{
|
|
171
|
+
"dependencies": {
|
|
172
|
+
"@sdxc/random": "2026.9.4"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
A caret or tilde range reads the date as major, minor and patch, so it accepts every later release in the same year. An exact version keeps the upgrade yours to schedule.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
MIT
|
|
182
|
+
|
|
183
|
+
## Author
|
|
184
|
+
|
|
185
|
+
[Sergio Xalambrí](https://sergiodxa.com)
|
package/dist/draws.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Random` interface and the draws built on a raw `[0, 1)` source. Every
|
|
3
|
+
* stream, seeded or system, shares these definitions, so `int`, `pick` and
|
|
4
|
+
* `shuffle` carry one set of bounds and one set of errors across the package.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/** A source of random values, and the draws that consume it. */
|
|
10
|
+
export interface Random {
|
|
11
|
+
/** The next raw draw, in `[0, 1)`. */
|
|
12
|
+
next(): number;
|
|
13
|
+
/**
|
|
14
|
+
* An integer in `[min, max]`, both ends included.
|
|
15
|
+
*
|
|
16
|
+
* @throws RangeError when a bound is not a safe integer, or `max` is below
|
|
17
|
+
* `min`.
|
|
18
|
+
*/
|
|
19
|
+
int(min: number, max: number): number;
|
|
20
|
+
/** A number in `[min, max)`, defaulting to `[0, 1)`. */
|
|
21
|
+
float(min?: number, max?: number): number;
|
|
22
|
+
/** `true` with probability `chance`, half the time by default. */
|
|
23
|
+
bool(chance?: number): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* One element, every element equally likely.
|
|
26
|
+
*
|
|
27
|
+
* @throws RangeError when the list is empty.
|
|
28
|
+
*/
|
|
29
|
+
pick<T>(items: readonly T[]): T;
|
|
30
|
+
/** A shuffled copy; the argument keeps its order. */
|
|
31
|
+
shuffle<T>(items: readonly T[]): T[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build every draw on top of `next`. Each draw consumes a fixed number of raw
|
|
35
|
+
* values for a given input, which is what keeps a seeded sequence stable.
|
|
36
|
+
*
|
|
37
|
+
* @param next - The raw source, returning values in `[0, 1)`.
|
|
38
|
+
* @returns The draws, all reading from `next`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function drawsFrom(next: () => number): Random;
|
package/dist/draws.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `Random` interface and the draws built on a raw `[0, 1)` source. Every
|
|
3
|
+
* stream, seeded or system, shares these definitions, so `int`, `pick` and
|
|
4
|
+
* `shuffle` carry one set of bounds and one set of errors across the package.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Build every draw on top of `next`. Each draw consumes a fixed number of raw
|
|
11
|
+
* values for a given input, which is what keeps a seeded sequence stable.
|
|
12
|
+
*
|
|
13
|
+
* @param next - The raw source, returning values in `[0, 1)`.
|
|
14
|
+
* @returns The draws, all reading from `next`.
|
|
15
|
+
*/
|
|
16
|
+
export function drawsFrom(next) {
|
|
17
|
+
let random = {
|
|
18
|
+
next,
|
|
19
|
+
int(min, max) {
|
|
20
|
+
if (!Number.isSafeInteger(min) || !Number.isSafeInteger(max)) {
|
|
21
|
+
throw new RangeError(`int() needs safe integer bounds, received ${min} and ${max}.`);
|
|
22
|
+
}
|
|
23
|
+
if (max < min)
|
|
24
|
+
throw new RangeError(`int() needs max (${max}) to be at least min (${min}).`);
|
|
25
|
+
return min + Math.floor(next() * (max - min + 1));
|
|
26
|
+
},
|
|
27
|
+
float(min = 0, max = 1) {
|
|
28
|
+
return min + next() * (max - min);
|
|
29
|
+
},
|
|
30
|
+
bool(chance = 0.5) {
|
|
31
|
+
return next() < chance;
|
|
32
|
+
},
|
|
33
|
+
pick(items) {
|
|
34
|
+
if (items.length === 0)
|
|
35
|
+
throw new RangeError("pick() needs a list with at least one item.");
|
|
36
|
+
return items[random.int(0, items.length - 1)];
|
|
37
|
+
},
|
|
38
|
+
shuffle(items) {
|
|
39
|
+
let copy = [...items];
|
|
40
|
+
for (let index = copy.length - 1; index > 0; index--) {
|
|
41
|
+
let target = random.int(0, index);
|
|
42
|
+
let held = copy[index];
|
|
43
|
+
copy[index] = copy[target];
|
|
44
|
+
copy[target] = held;
|
|
45
|
+
}
|
|
46
|
+
return copy;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return random;
|
|
50
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package entrypoint: the `Random` interface a consumer accepts, the seeded
|
|
3
|
+
* stream a caller replays or persists, and the Web Crypto stream for values that
|
|
4
|
+
* only need a good spread. It has no dependencies; the snapshot parser lives in
|
|
5
|
+
* `@sdxc/random/schema`.
|
|
6
|
+
*
|
|
7
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
8
|
+
* @copyright Sergio Xalambrí 2026
|
|
9
|
+
*/
|
|
10
|
+
export type { Random } from "./draws.js";
|
|
11
|
+
export type { RandomState, Seed, SeededRandom } from "./seeded.js";
|
|
12
|
+
export { createRandom, restoreRandom } from "./seeded.js";
|
|
13
|
+
export { systemRandom, systemSeed } from "./system.js";
|
package/dist/index.js
ADDED
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The parser for a `RandomState` read back from storage, on its own entry point
|
|
3
|
+
* so only a caller that parses snapshots installs `remix`. A save file is
|
|
4
|
+
* untrusted input, and a malformed snapshot would resume a different stream.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Schema } from "remix/data-schema";
|
|
10
|
+
import type { RandomState } from "./seeded.js";
|
|
11
|
+
/**
|
|
12
|
+
* Reads a snapshot from `state()`, so `restoreRandom` receives exactly the
|
|
13
|
+
* shape it was written in.
|
|
14
|
+
*
|
|
15
|
+
* @example s.parseSafe(RANDOM_STATE_SCHEMA, JSON.parse(stored))
|
|
16
|
+
*/
|
|
17
|
+
export declare const RANDOM_STATE_SCHEMA: Schema<unknown, RandomState>;
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as s from "remix/data-schema";
|
|
2
|
+
/** Reads one generator word, which the stream stores as an unsigned 32-bit integer. */
|
|
3
|
+
const WORD_SCHEMA = s
|
|
4
|
+
.number()
|
|
5
|
+
.refine((value) => Number.isInteger(value) && value >= 0 && value <= 0xffffffff, "Expected an unsigned 32-bit integer");
|
|
6
|
+
/**
|
|
7
|
+
* Reads a snapshot from `state()`, so `restoreRandom` receives exactly the
|
|
8
|
+
* shape it was written in.
|
|
9
|
+
*
|
|
10
|
+
* @example s.parseSafe(RANDOM_STATE_SCHEMA, JSON.parse(stored))
|
|
11
|
+
*/
|
|
12
|
+
export const RANDOM_STATE_SCHEMA = s.object({
|
|
13
|
+
seed: s.union([s.string(), s.number()]),
|
|
14
|
+
words: s.tuple([
|
|
15
|
+
WORD_SCHEMA,
|
|
16
|
+
WORD_SCHEMA,
|
|
17
|
+
WORD_SCHEMA,
|
|
18
|
+
WORD_SCHEMA,
|
|
19
|
+
]),
|
|
20
|
+
});
|
package/dist/seeded.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seeded stream: output depends on the seed and on how many draws have been
|
|
3
|
+
* taken, and on nothing else, so a seed replays the same sequence on any machine.
|
|
4
|
+
* Its four words of state can be saved and restored to resume a stream mid-way.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Random } from "./draws.js";
|
|
10
|
+
/** A seed as a caller supplies it; text and numbers both name a stream. */
|
|
11
|
+
export type Seed = number | string;
|
|
12
|
+
/**
|
|
13
|
+
* Where a seeded stream stands. It is plain JSON, so it can be stored beside the
|
|
14
|
+
* data it produced and handed back to `restoreRandom` later.
|
|
15
|
+
*/
|
|
16
|
+
export interface RandomState {
|
|
17
|
+
/** The seed the stream was opened on, which `derive` keeps building from. */
|
|
18
|
+
seed: Seed;
|
|
19
|
+
/** The generator's four unsigned 32-bit words. */
|
|
20
|
+
words: [number, number, number, number];
|
|
21
|
+
}
|
|
22
|
+
/** A stream that can be replayed, derived from and persisted. */
|
|
23
|
+
export interface SeededRandom extends Random {
|
|
24
|
+
/** The seed this stream replays from. */
|
|
25
|
+
readonly seed: Seed;
|
|
26
|
+
/**
|
|
27
|
+
* An independent stream named by `label`. It is seeded from this stream's
|
|
28
|
+
* seed and the label alone, so its values hold still no matter how many
|
|
29
|
+
* draws this stream has taken or takes later.
|
|
30
|
+
*/
|
|
31
|
+
derive(label: string): SeededRandom;
|
|
32
|
+
/** A snapshot that `restoreRandom` resumes from the next draw on. */
|
|
33
|
+
state(): RandomState;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Open a stream on a seed. Two streams on the same seed produce the same values
|
|
37
|
+
* in the same order, which is what lets a run be reproduced from the seed alone.
|
|
38
|
+
*
|
|
39
|
+
* @param seed - Text or a number naming the stream to replay.
|
|
40
|
+
* @returns The stream, positioned at its first value.
|
|
41
|
+
* @example createRandom("battle-42").int(1, 6)
|
|
42
|
+
*/
|
|
43
|
+
export declare function createRandom(seed: Seed): SeededRandom;
|
|
44
|
+
/**
|
|
45
|
+
* Resume a stream from a snapshot. The restored stream draws exactly what the
|
|
46
|
+
* original would have drawn next, and derives the same streams it would have.
|
|
47
|
+
*
|
|
48
|
+
* @param state - A snapshot from `state()`; validate one read from storage with `RANDOM_STATE_SCHEMA`.
|
|
49
|
+
* @returns The stream, positioned where the snapshot was taken.
|
|
50
|
+
* @example restoreRandom(saved.random).int(1, 6)
|
|
51
|
+
*/
|
|
52
|
+
export declare function restoreRandom(state: RandomState): SeededRandom;
|
package/dist/seeded.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { drawsFrom } from "./draws.js";
|
|
2
|
+
/** Joins a seed to a label so `derive` composes into a readable seed. */
|
|
3
|
+
const LABEL_SEPARATOR = " ";
|
|
4
|
+
/**
|
|
5
|
+
* Hash text into the four 32-bit words the generator starts from, so a seed can
|
|
6
|
+
* be anything a caller finds meaningful and still spreads across the state.
|
|
7
|
+
*/
|
|
8
|
+
function hashSeed(seed) {
|
|
9
|
+
let text = String(seed);
|
|
10
|
+
let a = 1779033703;
|
|
11
|
+
let b = 3144134277;
|
|
12
|
+
let c = 1013904242;
|
|
13
|
+
let d = 2773480762;
|
|
14
|
+
for (let index = 0; index < text.length; index++) {
|
|
15
|
+
let code = text.charCodeAt(index);
|
|
16
|
+
a = b ^ Math.imul(a ^ code, 597399067);
|
|
17
|
+
b = c ^ Math.imul(b ^ code, 2869860233);
|
|
18
|
+
c = d ^ Math.imul(c ^ code, 951274213);
|
|
19
|
+
d = a ^ Math.imul(d ^ code, 2716044179);
|
|
20
|
+
}
|
|
21
|
+
a = Math.imul(c ^ (a >>> 18), 597399067);
|
|
22
|
+
b = Math.imul(d ^ (b >>> 22), 2869860233);
|
|
23
|
+
c = Math.imul(a ^ (c >>> 17), 951274213);
|
|
24
|
+
d = Math.imul(b ^ (d >>> 19), 2716044179);
|
|
25
|
+
return [(a ^ b ^ c ^ d) >>> 0, (b ^ a) >>> 0, (c ^ a) >>> 0, (d ^ a) >>> 0];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Open the generator on `seed` positioned at `words`: four words of state
|
|
29
|
+
* advanced per draw, which buys a long period and a flat distribution for a
|
|
30
|
+
* handful of integer operations.
|
|
31
|
+
*/
|
|
32
|
+
function seededStream(seed, words) {
|
|
33
|
+
let [a, b, c, d] = words;
|
|
34
|
+
/** Advance the state one step and read the draw it produced. */
|
|
35
|
+
function next() {
|
|
36
|
+
a |= 0;
|
|
37
|
+
b |= 0;
|
|
38
|
+
c |= 0;
|
|
39
|
+
d |= 0;
|
|
40
|
+
let sum = (((a + b) | 0) + d) | 0;
|
|
41
|
+
d = (d + 1) | 0;
|
|
42
|
+
a = b ^ (b >>> 9);
|
|
43
|
+
b = (c + (c << 3)) | 0;
|
|
44
|
+
c = (c << 21) | (c >>> 11);
|
|
45
|
+
c = (c + sum) | 0;
|
|
46
|
+
return (sum >>> 0) / 4294967296;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...drawsFrom(next),
|
|
50
|
+
seed,
|
|
51
|
+
derive(label) {
|
|
52
|
+
return createRandom(`${seed}${LABEL_SEPARATOR}${label}`);
|
|
53
|
+
},
|
|
54
|
+
state() {
|
|
55
|
+
return { seed, words: [a >>> 0, b >>> 0, c >>> 0, d >>> 0] };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Open a stream on a seed. Two streams on the same seed produce the same values
|
|
61
|
+
* in the same order, which is what lets a run be reproduced from the seed alone.
|
|
62
|
+
*
|
|
63
|
+
* @param seed - Text or a number naming the stream to replay.
|
|
64
|
+
* @returns The stream, positioned at its first value.
|
|
65
|
+
* @example createRandom("battle-42").int(1, 6)
|
|
66
|
+
*/
|
|
67
|
+
export function createRandom(seed) {
|
|
68
|
+
return seededStream(seed, hashSeed(seed));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resume a stream from a snapshot. The restored stream draws exactly what the
|
|
72
|
+
* original would have drawn next, and derives the same streams it would have.
|
|
73
|
+
*
|
|
74
|
+
* @param state - A snapshot from `state()`; validate one read from storage with `RANDOM_STATE_SCHEMA`.
|
|
75
|
+
* @returns The stream, positioned where the snapshot was taken.
|
|
76
|
+
* @example restoreRandom(saved.random).int(1, 6)
|
|
77
|
+
*/
|
|
78
|
+
export function restoreRandom(state) {
|
|
79
|
+
return seededStream(state.seed, [...state.words]);
|
|
80
|
+
}
|
package/dist/system.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Randomness drawn from `crypto.getRandomValues`: a well-spread source for
|
|
3
|
+
* jitter, sampling and short tie-breakers, and the fresh seed a reproducible run
|
|
4
|
+
* starts from. Both work in every runtime that ships Web Crypto.
|
|
5
|
+
*
|
|
6
|
+
* @author [Sergio Xalambrí](https://sergiodxa.com)
|
|
7
|
+
* @copyright Sergio Xalambrí 2026
|
|
8
|
+
*/
|
|
9
|
+
import type { Random } from "./draws.js";
|
|
10
|
+
/**
|
|
11
|
+
* Open an unseeded stream. Each draw is a fresh 32-bit value from Web Crypto,
|
|
12
|
+
* so its sequence never repeats across calls, processes or runs.
|
|
13
|
+
*
|
|
14
|
+
* @returns A stream with the same draws a seeded stream offers.
|
|
15
|
+
* @example systemRandom().float(0.8, 1.2)
|
|
16
|
+
*/
|
|
17
|
+
export declare function systemRandom(): Random;
|
|
18
|
+
/**
|
|
19
|
+
* Draw a seed from Web Crypto, for a process that wants fresh values on every
|
|
20
|
+
* run. Log the returned number and that run stays reproducible by passing it back.
|
|
21
|
+
*
|
|
22
|
+
* @returns A 32-bit seed.
|
|
23
|
+
* @example createRandom(systemSeed())
|
|
24
|
+
*/
|
|
25
|
+
export declare function systemSeed(): number;
|
package/dist/system.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { drawsFrom } from "./draws.js";
|
|
2
|
+
/** Words fetched per refill, so a burst of draws costs one Web Crypto call. */
|
|
3
|
+
const BUFFER_WORDS = 64;
|
|
4
|
+
/**
|
|
5
|
+
* Open an unseeded stream. Each draw is a fresh 32-bit value from Web Crypto,
|
|
6
|
+
* so its sequence never repeats across calls, processes or runs.
|
|
7
|
+
*
|
|
8
|
+
* @returns A stream with the same draws a seeded stream offers.
|
|
9
|
+
* @example systemRandom().float(0.8, 1.2)
|
|
10
|
+
*/
|
|
11
|
+
export function systemRandom() {
|
|
12
|
+
let buffer = new Uint32Array(BUFFER_WORDS);
|
|
13
|
+
let position = BUFFER_WORDS;
|
|
14
|
+
return drawsFrom(function next() {
|
|
15
|
+
if (position === BUFFER_WORDS) {
|
|
16
|
+
crypto.getRandomValues(buffer);
|
|
17
|
+
position = 0;
|
|
18
|
+
}
|
|
19
|
+
return buffer[position++] / 4294967296;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Draw a seed from Web Crypto, for a process that wants fresh values on every
|
|
24
|
+
* run. Log the returned number and that run stays reproducible by passing it back.
|
|
25
|
+
*
|
|
26
|
+
* @returns A 32-bit seed.
|
|
27
|
+
* @example createRandom(systemSeed())
|
|
28
|
+
*/
|
|
29
|
+
export function systemSeed() {
|
|
30
|
+
return crypto.getRandomValues(new Uint32Array(1))[0];
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sdxc/random",
|
|
3
|
+
"version": "0.0.0-pre.1",
|
|
4
|
+
"description": "Seeded and system random streams with integer, float, pick and shuffle draws, and resumable state",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./schema": "./dist/schema.js"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"remix": "3.0.0"
|
|
13
|
+
},
|
|
14
|
+
"peerDependenciesMeta": {
|
|
15
|
+
"remix": {
|
|
16
|
+
"optional": true
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"gitHead": "47e70835fe3943a13cf37f07c3dfb8203aba1c35",
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/sergiodxa/monorepo.git",
|
|
26
|
+
"directory": "packages/random"
|
|
27
|
+
}
|
|
28
|
+
}
|