@xtia/alea-rc 0.0.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/README.md +61 -0
- package/browser.d.ts +2 -0
- package/browser.js +3 -0
- package/common.d.ts +5 -0
- package/common.js +4 -0
- package/internal/alea.d.ts +130 -0
- package/internal/alea.js +232 -0
- package/internal/factories.d.ts +34 -0
- package/internal/factories.js +48 -0
- package/internal/util.d.ts +15 -0
- package/internal/util.js +63 -0
- package/node.d.ts +2 -0
- package/node.js +6 -0
- package/other.d.ts +6 -0
- package/other.js +8 -0
- package/package.json +38 -0
- package/prng/index.d.ts +3 -0
- package/prng/index.js +3 -0
- package/prng/mulberry32.d.ts +9 -0
- package/prng/mulberry32.js +19 -0
- package/prng/sfc32.d.ts +12 -0
- package/prng/sfc32.js +31 -0
- package/prng/xoshiro128pp.d.ts +12 -0
- package/prng/xoshiro128pp.js +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Alea
|
|
2
|
+
|
|
3
|
+
### Tame the Chaos
|
|
4
|
+
|
|
5
|
+
Alea is a utility wrapper for turning random numbers into useful values. Give it any source of randomness, get a toolkit for dice, samples, strings, and more.
|
|
6
|
+
|
|
7
|
+
* Fully typed
|
|
8
|
+
* Crypto-safe and seeded algorithms out-of-the-box
|
|
9
|
+
* No dependencies
|
|
10
|
+
* ~2.7kb minified
|
|
11
|
+
* Ranged int, array shuffling, dice roll, weighted sampling, phrase generation, uuid, bytes and many more
|
|
12
|
+
|
|
13
|
+
## Brief:
|
|
14
|
+
|
|
15
|
+
`npm i @xtia/alea` (pending release; use `@xtia/alea-rc` to preview)
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { alea, cryptoAlea } from "@xtia/alea";
|
|
19
|
+
|
|
20
|
+
// generate values (driven by Math.random())
|
|
21
|
+
const damage = alea.roll(2, 6); // 2d6
|
|
22
|
+
const duration = alea.between(1000, 1500);
|
|
23
|
+
const loot = alea.chance(0.125) ? "epic" : "common";
|
|
24
|
+
const id = alea.string(5);
|
|
25
|
+
const npcName = alea.sample(["Alice", "Bob", "Charlie"]);
|
|
26
|
+
|
|
27
|
+
// secure source (driven by environment's crypto)
|
|
28
|
+
const key = cryptoAlea.string(16);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Custom sources
|
|
32
|
+
|
|
33
|
+
Use any provider as RNG source:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
createAleaFromFunc,
|
|
38
|
+
createAleaFromSeed,
|
|
39
|
+
createAleaFromByteSource,
|
|
40
|
+
} from "@xtia/alea";
|
|
41
|
+
|
|
42
|
+
const deterministic = createAleaFromSeed("abc123");
|
|
43
|
+
const xkcdRng = createAleaFromFunc(() => 4/6); // https://xkcd.com/221/
|
|
44
|
+
const secure = createAleaFromByteSource(
|
|
45
|
+
buf => hardwareRNG.fillRandomBytes(buf)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// or use provided PRNG algorithms:
|
|
49
|
+
import {
|
|
50
|
+
mulberry32,
|
|
51
|
+
sfc32,
|
|
52
|
+
xoshiro128pp,
|
|
53
|
+
} from "@xtia/alea/prng";
|
|
54
|
+
|
|
55
|
+
// each returns an Alea instance:
|
|
56
|
+
const fast = mulberry32("my-seed");
|
|
57
|
+
const varied = sfc32(1, 2, 3, 4);
|
|
58
|
+
const strong = xoshiro128pp(5, 6, 7, 8);
|
|
59
|
+
|
|
60
|
+
const reproducibleId = varied.string(5);
|
|
61
|
+
```
|
package/browser.d.ts
ADDED
package/browser.js
ADDED
package/common.d.ts
ADDED
package/common.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
type RandomFunction = () => number;
|
|
2
|
+
type PhraseFunc = (parse: (template: string) => string) => string;
|
|
3
|
+
export declare class Alea {
|
|
4
|
+
/**
|
|
5
|
+
* Generate a float between 0 and 1 (exclusive)
|
|
6
|
+
*/
|
|
7
|
+
readonly next: RandomFunction;
|
|
8
|
+
/**
|
|
9
|
+
* @param next Source RNG - a function that returns a value >= 0 and < 1
|
|
10
|
+
*/
|
|
11
|
+
constructor(next: RandomFunction);
|
|
12
|
+
/**
|
|
13
|
+
* Generate a series of normalised random values
|
|
14
|
+
* @param count
|
|
15
|
+
* @returns Random values
|
|
16
|
+
*/
|
|
17
|
+
batch(count: number): number[];
|
|
18
|
+
/**
|
|
19
|
+
* Pick a random item from an array
|
|
20
|
+
* @param items
|
|
21
|
+
* @returns Random item from an array
|
|
22
|
+
*/
|
|
23
|
+
sample<T>(items: ArrayLike<T>): T;
|
|
24
|
+
/**
|
|
25
|
+
* Pick a number of unique random items from an array
|
|
26
|
+
* @param items
|
|
27
|
+
* @param count
|
|
28
|
+
* @returns Random items from an array
|
|
29
|
+
*/
|
|
30
|
+
sample<T>(items: ArrayLike<T>, count: number): T[];
|
|
31
|
+
/**
|
|
32
|
+
* Get a boolean value with a (`probability` in 1) chance of being `true`
|
|
33
|
+
* @param probability
|
|
34
|
+
* @returns Random boolean
|
|
35
|
+
*/
|
|
36
|
+
chance(probability: number): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Get a shuffled copy of an array
|
|
39
|
+
* @param items
|
|
40
|
+
* @returns Shuffled copy of an array
|
|
41
|
+
*/
|
|
42
|
+
shuffle<T>(items: ArrayLike<T>): T[];
|
|
43
|
+
/**
|
|
44
|
+
* Get a value between `min` and `max`
|
|
45
|
+
* @param min Minimum value, *inclusive*
|
|
46
|
+
* @param max Maximum value, *exclusive*
|
|
47
|
+
* @returns Random value in range
|
|
48
|
+
*/
|
|
49
|
+
between(min: number, max: number): number;
|
|
50
|
+
/**
|
|
51
|
+
* Generate a random string, drawing a given character set
|
|
52
|
+
* @param length
|
|
53
|
+
* @param charset
|
|
54
|
+
* @returns Generated string
|
|
55
|
+
*/
|
|
56
|
+
string(length: number, charset?: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Generates a phrase from a table and a root string
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const message = alea.phrase({
|
|
62
|
+
* greeting: ["hello", "hi", "{int} blessings"],
|
|
63
|
+
* addressee: ["world", "planet", "{adjective} @xtia user"],
|
|
64
|
+
* adjective: ["beautiful", "wonderful"],
|
|
65
|
+
* int: () => alea.int(0, 9).toString(),
|
|
66
|
+
* }, "{greeting}, {addressee}!")
|
|
67
|
+
* ```
|
|
68
|
+
* @param table
|
|
69
|
+
* @param root
|
|
70
|
+
* @returns Generated phrase
|
|
71
|
+
*/
|
|
72
|
+
phrase(table: Record<string, ArrayLike<string> | string | PhraseFunc>, root: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* Create a factory to pick random items from a biased list
|
|
75
|
+
* @param table Candidate table, as an array of `[value, weight]` tuples
|
|
76
|
+
* @returns Random item factory
|
|
77
|
+
*/
|
|
78
|
+
createWeightedSampler<T>(table: [value: T, weight: number][]): {
|
|
79
|
+
sample: () => T;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Generate a sequence of bytes
|
|
83
|
+
* @param count
|
|
84
|
+
* @returns Random byte array
|
|
85
|
+
*/
|
|
86
|
+
bytes(count: number): Uint8Array<ArrayBuffer>;
|
|
87
|
+
/**
|
|
88
|
+
* Round a value up or down, according to probability defined by its non-integral part
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const rawDamage = weapon.damage / armour.protection;
|
|
92
|
+
* hp -= alea.round(rawDamage);
|
|
93
|
+
* // HP remains integer while law of averages applies fractional damage
|
|
94
|
+
* ```
|
|
95
|
+
* @param n
|
|
96
|
+
* @returns Randomly rounded value
|
|
97
|
+
*/
|
|
98
|
+
round(n: number): number;
|
|
99
|
+
/**
|
|
100
|
+
* Get a random gaussian normal value
|
|
101
|
+
* @param mean
|
|
102
|
+
* @param deviation
|
|
103
|
+
* @returns Random gaussian normal
|
|
104
|
+
*/
|
|
105
|
+
normal(mean: number, deviation: number): number;
|
|
106
|
+
/**
|
|
107
|
+
* Get a random integer value between `min` and `max`, inclusive.
|
|
108
|
+
* @param min Minimum value, **inclusive**
|
|
109
|
+
* @param max Maximum value, **inclusive**
|
|
110
|
+
* @returns Random int value
|
|
111
|
+
*/
|
|
112
|
+
int(min: number, max: number): number;
|
|
113
|
+
/**
|
|
114
|
+
* Roll dice
|
|
115
|
+
* @param count Number of dice to roll (default 1)
|
|
116
|
+
* @param sides Number of sides per die (default 6)
|
|
117
|
+
* @returns Dice result
|
|
118
|
+
*/
|
|
119
|
+
roll(count?: number, sides?: number): number;
|
|
120
|
+
/**
|
|
121
|
+
* Generate a random UUID (version 4)
|
|
122
|
+
*
|
|
123
|
+
* **Security note**: output is only as cryptographically secure as
|
|
124
|
+
* an instance's PRNG source, which, by default, is not.
|
|
125
|
+
* @see {@link Alea.crypto}
|
|
126
|
+
* @returns Random UUID string
|
|
127
|
+
*/
|
|
128
|
+
uuid(): string;
|
|
129
|
+
}
|
|
130
|
+
export {};
|
package/internal/alea.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { charsets } from "./util";
|
|
2
|
+
export class Alea {
|
|
3
|
+
/**
|
|
4
|
+
* @param next Source RNG - a function that returns a value >= 0 and < 1
|
|
5
|
+
*/
|
|
6
|
+
constructor(next) {
|
|
7
|
+
this.next = next;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Generate a series of normalised random values
|
|
11
|
+
* @param count
|
|
12
|
+
* @returns Random values
|
|
13
|
+
*/
|
|
14
|
+
batch(count) {
|
|
15
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
16
|
+
throw new RangeError("count must be a non-negative integer");
|
|
17
|
+
}
|
|
18
|
+
return Array.from({ length: count }, () => this.next());
|
|
19
|
+
}
|
|
20
|
+
sample(items, count) {
|
|
21
|
+
if (count === undefined) {
|
|
22
|
+
if (items.length === 0)
|
|
23
|
+
throw new RangeError("Empty sample source");
|
|
24
|
+
return items[Math.floor(this.next() * items.length)];
|
|
25
|
+
}
|
|
26
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
27
|
+
throw new RangeError("count must be a non-negative integer");
|
|
28
|
+
}
|
|
29
|
+
if (count > items.length) {
|
|
30
|
+
throw new RangeError(`Cannot sample ${count} items from array of length ${items.length}`);
|
|
31
|
+
}
|
|
32
|
+
const result = Array.from({ length: count }, (_, index) => items[index]);
|
|
33
|
+
for (let i = count; i < items.length; i++) {
|
|
34
|
+
const replaceIndex = Math.floor(this.next() * (i + 1));
|
|
35
|
+
if (replaceIndex < count) {
|
|
36
|
+
result[replaceIndex] = items[i];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Get a boolean value with a (`probability` in 1) chance of being `true`
|
|
43
|
+
* @param probability
|
|
44
|
+
* @returns Random boolean
|
|
45
|
+
*/
|
|
46
|
+
chance(probability) {
|
|
47
|
+
return this.next() < probability;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get a shuffled copy of an array
|
|
51
|
+
* @param items
|
|
52
|
+
* @returns Shuffled copy of an array
|
|
53
|
+
*/
|
|
54
|
+
shuffle(items) {
|
|
55
|
+
const shuffled = Array.from(items);
|
|
56
|
+
for (let i = shuffled.length - 1; i > 0; i--) {
|
|
57
|
+
const j = Math.floor(this.next() * (i + 1));
|
|
58
|
+
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
|
59
|
+
}
|
|
60
|
+
return shuffled;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get a value between `min` and `max`
|
|
64
|
+
* @param min Minimum value, *inclusive*
|
|
65
|
+
* @param max Maximum value, *exclusive*
|
|
66
|
+
* @returns Random value in range
|
|
67
|
+
*/
|
|
68
|
+
between(min, max) {
|
|
69
|
+
const range = max - min;
|
|
70
|
+
return min + range * this.next();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Generate a random string, drawing a given character set
|
|
74
|
+
* @param length
|
|
75
|
+
* @param charset
|
|
76
|
+
* @returns Generated string
|
|
77
|
+
*/
|
|
78
|
+
string(length, charset = charsets.alphanumericMixedCase) {
|
|
79
|
+
if (!Number.isInteger(length) || length < 0)
|
|
80
|
+
throw new RangeError("length must be a non-negative integer");
|
|
81
|
+
if (!charset || charset.length === 0)
|
|
82
|
+
throw new RangeError("charset must not be empty");
|
|
83
|
+
const chars = Array.from({ length }, () => charset[Math.floor(this.next() * charset.length)]);
|
|
84
|
+
return chars.join('');
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Generates a phrase from a table and a root string
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const message = alea.phrase({
|
|
91
|
+
* greeting: ["hello", "hi", "{int} blessings"],
|
|
92
|
+
* addressee: ["world", "planet", "{adjective} @xtia user"],
|
|
93
|
+
* adjective: ["beautiful", "wonderful"],
|
|
94
|
+
* int: () => alea.int(0, 9).toString(),
|
|
95
|
+
* }, "{greeting}, {addressee}!")
|
|
96
|
+
* ```
|
|
97
|
+
* @param table
|
|
98
|
+
* @param root
|
|
99
|
+
* @returns Generated phrase
|
|
100
|
+
*/
|
|
101
|
+
phrase(table, root) {
|
|
102
|
+
return root.replace(/\{([^}]+)\}/g, ((_, key) => {
|
|
103
|
+
if (table[key] === undefined)
|
|
104
|
+
return `{${key}}`;
|
|
105
|
+
if (typeof table[key] == "function") {
|
|
106
|
+
return table[key](template => this.phrase(table, template));
|
|
107
|
+
}
|
|
108
|
+
const source = table[key];
|
|
109
|
+
if (typeof source == "string")
|
|
110
|
+
return this.phrase(table, source);
|
|
111
|
+
return this.phrase(table, this.sample(source));
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Create a factory to pick random items from a biased list
|
|
116
|
+
* @param table Candidate table, as an array of `[value, weight]` tuples
|
|
117
|
+
* @returns Random item factory
|
|
118
|
+
*/
|
|
119
|
+
createWeightedSampler(table) {
|
|
120
|
+
const filtered = table.filter(v => Number.isFinite(v[1]) && v[1] > 0);
|
|
121
|
+
if (filtered.length === 0) {
|
|
122
|
+
throw new Error("Weighted source has no viable candidates");
|
|
123
|
+
}
|
|
124
|
+
const cumulative = new Array(filtered.length);
|
|
125
|
+
let total = 0;
|
|
126
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
127
|
+
total += filtered[i][1];
|
|
128
|
+
cumulative[i] = total;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
sample: () => {
|
|
132
|
+
const r = this.between(0, total);
|
|
133
|
+
let lo = 0;
|
|
134
|
+
let hi = cumulative.length - 1;
|
|
135
|
+
while (lo < hi) {
|
|
136
|
+
const mid = (lo + hi) >>> 1;
|
|
137
|
+
if (r < cumulative[mid])
|
|
138
|
+
hi = mid;
|
|
139
|
+
else
|
|
140
|
+
lo = mid + 1;
|
|
141
|
+
}
|
|
142
|
+
return filtered[lo][0];
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Generate a sequence of bytes
|
|
148
|
+
* @param count
|
|
149
|
+
* @returns Random byte array
|
|
150
|
+
*/
|
|
151
|
+
bytes(count) {
|
|
152
|
+
const arr = new Uint8Array(count);
|
|
153
|
+
for (let i = 0; i < count; i++)
|
|
154
|
+
arr[i] = this.int(0, 255);
|
|
155
|
+
return arr;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Round a value up or down, according to probability defined by its non-integral part
|
|
159
|
+
* @example
|
|
160
|
+
* ```ts
|
|
161
|
+
* const rawDamage = weapon.damage / armour.protection;
|
|
162
|
+
* hp -= alea.round(rawDamage);
|
|
163
|
+
* // HP remains integer while law of averages applies fractional damage
|
|
164
|
+
* ```
|
|
165
|
+
* @param n
|
|
166
|
+
* @returns Randomly rounded value
|
|
167
|
+
*/
|
|
168
|
+
round(n) {
|
|
169
|
+
const floor = Math.floor(n);
|
|
170
|
+
return this.chance(n - floor) ? floor + 1 : floor;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get a random gaussian normal value
|
|
174
|
+
* @param mean
|
|
175
|
+
* @param deviation
|
|
176
|
+
* @returns Random gaussian normal
|
|
177
|
+
*/
|
|
178
|
+
normal(mean, deviation) {
|
|
179
|
+
let u1 = this.next();
|
|
180
|
+
while (u1 <= Number.EPSILON) {
|
|
181
|
+
u1 = this.next();
|
|
182
|
+
}
|
|
183
|
+
const u2 = this.next();
|
|
184
|
+
const mag = Math.sqrt(-2 * Math.log(u1));
|
|
185
|
+
const angle = 2 * Math.PI * u2;
|
|
186
|
+
return mean + mag * Math.cos(angle) * deviation;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get a random integer value between `min` and `max`, inclusive.
|
|
190
|
+
* @param min Minimum value, **inclusive**
|
|
191
|
+
* @param max Maximum value, **inclusive**
|
|
192
|
+
* @returns Random int value
|
|
193
|
+
*/
|
|
194
|
+
int(min, max) {
|
|
195
|
+
return Math.floor(this.between(min, max + 1));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Roll dice
|
|
199
|
+
* @param count Number of dice to roll (default 1)
|
|
200
|
+
* @param sides Number of sides per die (default 6)
|
|
201
|
+
* @returns Dice result
|
|
202
|
+
*/
|
|
203
|
+
roll(count = 1, sides = 6) {
|
|
204
|
+
let total = 0;
|
|
205
|
+
for (let i = 0; i < count; i++) {
|
|
206
|
+
total += this.int(1, sides);
|
|
207
|
+
}
|
|
208
|
+
return total;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generate a random UUID (version 4)
|
|
212
|
+
*
|
|
213
|
+
* **Security note**: output is only as cryptographically secure as
|
|
214
|
+
* an instance's PRNG source, which, by default, is not.
|
|
215
|
+
* @see {@link Alea.crypto}
|
|
216
|
+
* @returns Random UUID string
|
|
217
|
+
*/
|
|
218
|
+
uuid() {
|
|
219
|
+
// xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
220
|
+
const bytes = this.bytes(16);
|
|
221
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
222
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
223
|
+
const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
224
|
+
return [
|
|
225
|
+
hex.slice(0, 8),
|
|
226
|
+
hex.slice(8, 12),
|
|
227
|
+
hex.slice(12, 16),
|
|
228
|
+
hex.slice(16, 20),
|
|
229
|
+
hex.slice(20, 32)
|
|
230
|
+
].join('-');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Alea } from "./alea";
|
|
2
|
+
/**
|
|
3
|
+
* Create an Alea instance using a byte generator, such as `crypto`,
|
|
4
|
+
* as a RNG source
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const cryptoAlea = createAleaFromByteSource(crypto.getRandomValues);
|
|
8
|
+
* const hwAlea = createAleaFromByteSource(hardwareRng.fillBytes);
|
|
9
|
+
* ```
|
|
10
|
+
* @param applyBytes A callback that fills a Uint8Array with random bytes
|
|
11
|
+
* @returns A byte generator-sourced Alea instance
|
|
12
|
+
*/
|
|
13
|
+
export declare function createAleaFromByteSource(applyBytes: (buffer: Uint8Array) => void): Alea;
|
|
14
|
+
/**
|
|
15
|
+
* Create an Alea instance using a Mulberry32 source
|
|
16
|
+
*
|
|
17
|
+
* Fast, with decent statistical quality
|
|
18
|
+
*
|
|
19
|
+
* For applications requiring higher statistical quality or different characteristics, see the specialized PRNGs in @xtia/Alea/prng
|
|
20
|
+
* @param seed
|
|
21
|
+
* @returns Alea instance using Mulberry32
|
|
22
|
+
*/
|
|
23
|
+
export declare function createAleaFromSeed(seed: number | string): Alea;
|
|
24
|
+
/**
|
|
25
|
+
* Create an Alea instance using a custom function as an RNG source
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* const basicAlea = createAleaFromFunc(Math.random);
|
|
29
|
+
* const lcgAlea = createAleaFromFunc(customRng.next);
|
|
30
|
+
* ```
|
|
31
|
+
* @param fn Source RNG; a function that returns a value >= 0 and < 1
|
|
32
|
+
* @returns Custom function-sourced Alea instance
|
|
33
|
+
*/
|
|
34
|
+
export declare function createAleaFromFunc(fn: () => number): Alea;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mulberry32 } from "../prng";
|
|
2
|
+
import { Alea } from "./alea";
|
|
3
|
+
/**
|
|
4
|
+
* Create an Alea instance using a byte generator, such as `crypto`,
|
|
5
|
+
* as a RNG source
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const cryptoAlea = createAleaFromByteSource(crypto.getRandomValues);
|
|
9
|
+
* const hwAlea = createAleaFromByteSource(hardwareRng.fillBytes);
|
|
10
|
+
* ```
|
|
11
|
+
* @param applyBytes A callback that fills a Uint8Array with random bytes
|
|
12
|
+
* @returns A byte generator-sourced Alea instance
|
|
13
|
+
*/
|
|
14
|
+
export function createAleaFromByteSource(applyBytes) {
|
|
15
|
+
const buffer = new ArrayBuffer(4);
|
|
16
|
+
const view = new Uint8Array(buffer);
|
|
17
|
+
const uint32View = new Uint32Array(buffer);
|
|
18
|
+
return new Alea(() => {
|
|
19
|
+
applyBytes(view);
|
|
20
|
+
return uint32View[0] / 4294967296;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create an Alea instance using a Mulberry32 source
|
|
25
|
+
*
|
|
26
|
+
* Fast, with decent statistical quality
|
|
27
|
+
*
|
|
28
|
+
* For applications requiring higher statistical quality or different characteristics, see the specialized PRNGs in @xtia/Alea/prng
|
|
29
|
+
* @param seed
|
|
30
|
+
* @returns Alea instance using Mulberry32
|
|
31
|
+
*/
|
|
32
|
+
export function createAleaFromSeed(seed) {
|
|
33
|
+
return mulberry32(seed);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create an Alea instance using a custom function as an RNG source
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const basicAlea = createAleaFromFunc(Math.random);
|
|
40
|
+
* const lcgAlea = createAleaFromFunc(customRng.next);
|
|
41
|
+
* ```
|
|
42
|
+
* @param fn Source RNG; a function that returns a value >= 0 and < 1
|
|
43
|
+
* @returns Custom function-sourced Alea instance
|
|
44
|
+
*/
|
|
45
|
+
export function createAleaFromFunc(fn) {
|
|
46
|
+
return new Alea(fn);
|
|
47
|
+
}
|
|
48
|
+
// const xkcdAlea = createAleaFromFunc(() => 4/6); // decided by die roll
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function murmur3_32(key: string, seed?: number): number;
|
|
2
|
+
export declare const charsets: {
|
|
3
|
+
lowercase: string;
|
|
4
|
+
uppercase: string;
|
|
5
|
+
numbers: string;
|
|
6
|
+
hexadecimalUppercase: string;
|
|
7
|
+
hexadecimalLowercase: string;
|
|
8
|
+
alphanumericUppercase: string;
|
|
9
|
+
alphanumericLowercase: string;
|
|
10
|
+
alphanumericMixedCase: string;
|
|
11
|
+
urlSafe: string;
|
|
12
|
+
wide: string;
|
|
13
|
+
exclude: (charset: string, excluded: string) => string;
|
|
14
|
+
unique: (charset: string) => string;
|
|
15
|
+
};
|
package/internal/util.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function murmur3_32(key, seed = 0) {
|
|
2
|
+
let remainder = key.length & 3;
|
|
3
|
+
let bytes = key.length - remainder;
|
|
4
|
+
let h1 = seed >>> 0;
|
|
5
|
+
const c1 = 0xcc9e2d51;
|
|
6
|
+
const c2 = 0x1b873593;
|
|
7
|
+
let i = 0;
|
|
8
|
+
while (i < bytes) {
|
|
9
|
+
let k1 = (key.charCodeAt(i) & 0xff)
|
|
10
|
+
| ((key.charCodeAt(i + 1) & 0xff) << 8)
|
|
11
|
+
| ((key.charCodeAt(i + 2) & 0xff) << 16)
|
|
12
|
+
| ((key.charCodeAt(i + 3) & 0xff) << 24);
|
|
13
|
+
i += 4;
|
|
14
|
+
k1 = Math.imul(k1, c1);
|
|
15
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
16
|
+
k1 = Math.imul(k1, c2);
|
|
17
|
+
h1 ^= k1;
|
|
18
|
+
h1 = (h1 << 13) | (h1 >>> 19);
|
|
19
|
+
h1 = (Math.imul(h1, 5) + 0xe6546b64) | 0;
|
|
20
|
+
}
|
|
21
|
+
let k1 = 0;
|
|
22
|
+
switch (remainder) {
|
|
23
|
+
case 3:
|
|
24
|
+
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
|
|
25
|
+
case 2:
|
|
26
|
+
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
|
|
27
|
+
case 1:
|
|
28
|
+
k1 ^= (key.charCodeAt(i) & 0xff);
|
|
29
|
+
k1 = Math.imul(k1, c1);
|
|
30
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
31
|
+
k1 = Math.imul(k1, c2);
|
|
32
|
+
h1 ^= k1;
|
|
33
|
+
}
|
|
34
|
+
h1 ^= key.length;
|
|
35
|
+
h1 ^= h1 >>> 16;
|
|
36
|
+
h1 = Math.imul(h1, 0x85ebca6b);
|
|
37
|
+
h1 ^= h1 >>> 13;
|
|
38
|
+
h1 = Math.imul(h1, 0xc2b2ae35);
|
|
39
|
+
h1 ^= h1 >>> 16;
|
|
40
|
+
return h1 >>> 0;
|
|
41
|
+
}
|
|
42
|
+
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
|
43
|
+
const uppercase = lowercase.toUpperCase();
|
|
44
|
+
const numbers = "0123456789";
|
|
45
|
+
export const charsets = {
|
|
46
|
+
lowercase,
|
|
47
|
+
uppercase,
|
|
48
|
+
numbers,
|
|
49
|
+
hexadecimalUppercase: numbers + "ABCDEF",
|
|
50
|
+
hexadecimalLowercase: numbers + "abcdef",
|
|
51
|
+
alphanumericUppercase: uppercase + numbers,
|
|
52
|
+
alphanumericLowercase: lowercase + numbers,
|
|
53
|
+
alphanumericMixedCase: lowercase + uppercase + numbers,
|
|
54
|
+
urlSafe: uppercase + lowercase + numbers + "_-.~",
|
|
55
|
+
wide: uppercase + lowercase + numbers + "_-+=[]{};#:@~,./<>?!$%^&*()",
|
|
56
|
+
exclude: (charset, excluded) => {
|
|
57
|
+
const excludedSet = new Set([...excluded]);
|
|
58
|
+
return [...charset].filter(c => !excludedSet.has(c)).join("");
|
|
59
|
+
},
|
|
60
|
+
unique: (charset) => {
|
|
61
|
+
return [...new Set([...charset])].join("");
|
|
62
|
+
},
|
|
63
|
+
};
|
package/node.d.ts
ADDED
package/node.js
ADDED
package/other.d.ts
ADDED
package/other.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Alea } from "./internal/alea";
|
|
2
|
+
export * from "./common";
|
|
3
|
+
/**
|
|
4
|
+
* An Alea instance that uses the local environment's `crypto` provider
|
|
5
|
+
*/
|
|
6
|
+
export const cryptoAlea = new Alea(() => {
|
|
7
|
+
throw new Error("cryptoAlea is not available in this environment. Consider using createAleaFromByteSource() with your environment's crypto API.");
|
|
8
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xtia/alea-rc",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "RNG utilities",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/tiadrop/alea",
|
|
7
|
+
"type": "github"
|
|
8
|
+
},
|
|
9
|
+
"sideEffects": false,
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"main": "./index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./other.d.ts",
|
|
15
|
+
"browser": "./browser.js",
|
|
16
|
+
"workerd": "./browser.js",
|
|
17
|
+
"deno": "./browser.js",
|
|
18
|
+
"node": "./node.js",
|
|
19
|
+
"bun": "./node.js",
|
|
20
|
+
"default": "./other.js"
|
|
21
|
+
},
|
|
22
|
+
"./prng": {
|
|
23
|
+
"types": "./prng/index.d.ts",
|
|
24
|
+
"default": "./prng/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./internal/*": null
|
|
27
|
+
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"prepublishOnly": "cp ../README.md .",
|
|
30
|
+
"postpublish": "rm README.md"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"random",
|
|
34
|
+
"rng"
|
|
35
|
+
],
|
|
36
|
+
"author": "Aleta Lovelace",
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
package/prng/index.d.ts
ADDED
package/prng/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
/**
|
|
3
|
+
* Create an Alea instance using a Mulberry32 source
|
|
4
|
+
*
|
|
5
|
+
* Fast, with decent statistical quality
|
|
6
|
+
* @param seed
|
|
7
|
+
* @returns Alea instance using Mulberry32
|
|
8
|
+
*/
|
|
9
|
+
export declare const mulberry32: (seed: number | string) => Alea;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
import { murmur3_32 } from "../internal/util";
|
|
3
|
+
/**
|
|
4
|
+
* Create an Alea instance using a Mulberry32 source
|
|
5
|
+
*
|
|
6
|
+
* Fast, with decent statistical quality
|
|
7
|
+
* @param seed
|
|
8
|
+
* @returns Alea instance using Mulberry32
|
|
9
|
+
*/
|
|
10
|
+
export const mulberry32 = (seed) => {
|
|
11
|
+
let nseed = typeof seed == "string" ? murmur3_32(seed) : (seed >>> 0);
|
|
12
|
+
return new Alea(() => {
|
|
13
|
+
nseed |= 0;
|
|
14
|
+
nseed = nseed + 0x6D2B79F5 | 0;
|
|
15
|
+
let t = Math.imul(nseed ^ nseed >>> 15, 1 | nseed);
|
|
16
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
17
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
18
|
+
});
|
|
19
|
+
};
|
package/prng/sfc32.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
/**
|
|
3
|
+
* Create an Alea instance using a Small Fast Counter (SFC32) source
|
|
4
|
+
*
|
|
5
|
+
* Fairly fast, with high statistical quality
|
|
6
|
+
* @param a Seed A
|
|
7
|
+
* @param b Seed B
|
|
8
|
+
* @param c Seed C
|
|
9
|
+
* @param d Seed D
|
|
10
|
+
* @returns Alea isntance using SFC32
|
|
11
|
+
*/
|
|
12
|
+
export declare function sfc32(a: number | string, b: number | string, c: number | string, d: number | string): Alea;
|
package/prng/sfc32.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
import { murmur3_32 } from "../internal/util";
|
|
3
|
+
/**
|
|
4
|
+
* Create an Alea instance using a Small Fast Counter (SFC32) source
|
|
5
|
+
*
|
|
6
|
+
* Fairly fast, with high statistical quality
|
|
7
|
+
* @param a Seed A
|
|
8
|
+
* @param b Seed B
|
|
9
|
+
* @param c Seed C
|
|
10
|
+
* @param d Seed D
|
|
11
|
+
* @returns Alea isntance using SFC32
|
|
12
|
+
*/
|
|
13
|
+
export function sfc32(a, b, c, d) {
|
|
14
|
+
const toWord = (v) => typeof v === "number" ? (v >>> 0) : murmur3_32(String(v));
|
|
15
|
+
let s0 = toWord(a) | 0;
|
|
16
|
+
let s1 = toWord(b) | 0;
|
|
17
|
+
let s2 = toWord(c) | 0;
|
|
18
|
+
let s3 = toWord(d) | 0;
|
|
19
|
+
return new Alea(() => {
|
|
20
|
+
s0 |= 0;
|
|
21
|
+
s1 |= 0;
|
|
22
|
+
s2 |= 0;
|
|
23
|
+
s3 |= 0;
|
|
24
|
+
const t = (s0 + s1 | 0) + s3 | 0;
|
|
25
|
+
s3 = s3 + 1 | 0;
|
|
26
|
+
s0 = s1 ^ s1 >>> 9;
|
|
27
|
+
s1 = s2 + (s2 << 3) | 0;
|
|
28
|
+
s2 = (s2 << 21 | s2 >>> 11) + t | 0;
|
|
29
|
+
return (t >>> 0) / 4294967296;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
/**
|
|
3
|
+
* Create an Alea instance using a Xoshiro128++ source
|
|
4
|
+
*
|
|
5
|
+
* Very high statistical quality
|
|
6
|
+
* @param a Seed A
|
|
7
|
+
* @param b Seed B
|
|
8
|
+
* @param c Seed C
|
|
9
|
+
* @param d Seed D
|
|
10
|
+
* @returns Alea instance using Xoshiro128++
|
|
11
|
+
*/
|
|
12
|
+
export declare function xoshiro128pp(a: number | string, b: number | string, c: number | string, d: number | string): Alea;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Alea } from "../internal/alea";
|
|
2
|
+
import { murmur3_32 } from "../internal/util";
|
|
3
|
+
function rotl(x, k) {
|
|
4
|
+
return (x << k) | (x >>> (32 - k));
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Create an Alea instance using a Xoshiro128++ source
|
|
8
|
+
*
|
|
9
|
+
* Very high statistical quality
|
|
10
|
+
* @param a Seed A
|
|
11
|
+
* @param b Seed B
|
|
12
|
+
* @param c Seed C
|
|
13
|
+
* @param d Seed D
|
|
14
|
+
* @returns Alea instance using Xoshiro128++
|
|
15
|
+
*/
|
|
16
|
+
export function xoshiro128pp(a, b, c, d) {
|
|
17
|
+
const toWord = (v) => typeof v === "number" ? v >>> 0 : murmur3_32(String(v));
|
|
18
|
+
let s0 = toWord(a) | 0;
|
|
19
|
+
let s1 = toWord(b) | 0;
|
|
20
|
+
let s2 = toWord(c) | 0;
|
|
21
|
+
let s3 = toWord(d) | 0;
|
|
22
|
+
// requires at least one non-zero value
|
|
23
|
+
if (s0 === 0 && s1 === 0 && s2 === 0 && s3 === 0) {
|
|
24
|
+
s0 = 1;
|
|
25
|
+
}
|
|
26
|
+
return new Alea(() => {
|
|
27
|
+
s0 |= 0;
|
|
28
|
+
s1 |= 0;
|
|
29
|
+
s2 |= 0;
|
|
30
|
+
s3 |= 0;
|
|
31
|
+
const result = (rotl(s0 + s3, 7) + s0) >>> 0;
|
|
32
|
+
const t = s1 << 9;
|
|
33
|
+
s2 ^= s0;
|
|
34
|
+
s3 ^= s1;
|
|
35
|
+
s1 ^= s2;
|
|
36
|
+
s0 ^= s3;
|
|
37
|
+
s2 ^= t;
|
|
38
|
+
s3 = rotl(s3, 11);
|
|
39
|
+
return result / 4294967296;
|
|
40
|
+
});
|
|
41
|
+
}
|