@esportsplus/random 0.0.28 → 0.0.30

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 (51) hide show
  1. package/.github/dependabot.yml +2 -0
  2. package/.github/workflows/bump.yml +2 -0
  3. package/.github/workflows/dependabot.yml +12 -0
  4. package/.github/workflows/publish.yml +4 -2
  5. package/README.md +387 -0
  6. package/build/alphanumeric.d.ts +2 -0
  7. package/build/alphanumeric.js +12 -0
  8. package/build/between.d.ts +2 -0
  9. package/build/between.js +12 -0
  10. package/build/coinflip.d.ts +1 -1
  11. package/build/coinflip.js +3 -3
  12. package/build/exponential.d.ts +2 -0
  13. package/build/exponential.js +11 -0
  14. package/build/gaussian.d.ts +2 -0
  15. package/build/gaussian.js +12 -0
  16. package/build/groups.d.ts +2 -0
  17. package/build/groups.js +11 -0
  18. package/build/hex.d.ts +2 -0
  19. package/build/hex.js +12 -0
  20. package/build/index.d.ts +9 -1
  21. package/build/index.js +9 -1
  22. package/build/item.d.ts +1 -1
  23. package/build/item.js +12 -16
  24. package/build/pick.d.ts +2 -0
  25. package/build/pick.js +14 -0
  26. package/build/range.d.ts +1 -1
  27. package/build/range.js +7 -4
  28. package/build/rng.d.ts +5 -0
  29. package/build/rng.js +53 -0
  30. package/build/roll.d.ts +1 -1
  31. package/build/roll.js +6 -3
  32. package/build/sample.d.ts +2 -0
  33. package/build/sample.js +14 -0
  34. package/build/shuffle.d.ts +1 -1
  35. package/build/shuffle.js +6 -3
  36. package/package.json +6 -5
  37. package/src/alphanumeric.ts +20 -0
  38. package/src/between.ts +19 -0
  39. package/src/coinflip.ts +3 -3
  40. package/src/exponential.ts +19 -0
  41. package/src/gaussian.ts +22 -0
  42. package/src/groups.ts +19 -0
  43. package/src/hex.ts +19 -0
  44. package/src/index.ts +9 -1
  45. package/src/item.ts +13 -19
  46. package/src/pick.ts +21 -0
  47. package/src/range.ts +8 -4
  48. package/src/rng.ts +79 -0
  49. package/src/roll.ts +7 -3
  50. package/src/sample.ts +20 -0
  51. package/src/shuffle.ts +8 -4
@@ -4,11 +4,13 @@
4
4
  # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
5
 
6
6
  version: 2
7
+
7
8
  registries:
8
9
  npm-npmjs:
9
10
  token: ${{secrets.NPM_TOKEN}}
10
11
  type: npm-registry
11
12
  url: https://registry.npmjs.org
13
+
12
14
  updates:
13
15
  - package-ecosystem: "npm"
14
16
  directory: "/"
@@ -1,7 +1,9 @@
1
1
  name: bump
2
+
2
3
  on:
3
4
  push:
4
5
  branches: '**' # only trigger on branches, not on tags
6
+
5
7
  jobs:
6
8
  bump:
7
9
  uses: esportsplus/workflows/.github/workflows/bump.yml@main
@@ -0,0 +1,12 @@
1
+ name: dependabot automerge
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, labeled]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ automerge:
10
+ secrets:
11
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
12
+ uses: esportsplus/workflows/.github/workflows/dependabot.yml@main
@@ -1,4 +1,5 @@
1
1
  name: publish to npm
2
+
2
3
  on:
3
4
  release:
4
5
  types: [published]
@@ -7,8 +8,9 @@ on:
7
8
  workflows: [bump]
8
9
  types:
9
10
  - completed
11
+
10
12
  jobs:
11
13
  publish:
12
14
  secrets:
13
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_PUBLISHING }}
14
- uses: esportsplus/workflows/.github/workflows/publish.yml@main
15
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
16
+ uses: esportsplus/workflows/.github/workflows/publish.yml@main
package/README.md ADDED
@@ -0,0 +1,387 @@
1
+ # @esportsplus/random
2
+
3
+ Cryptographically secure random utilities with optional seeded/provably fair mode.
4
+
5
+ All functions accept an optional `seed` parameter (number or string) as the last argument for reproducible results.
6
+
7
+ ---
8
+
9
+ ## Functions
10
+
11
+ ### `alphanumeric(length, seed?)`
12
+ Generate random alphanumeric string.
13
+
14
+ ```ts
15
+ alphanumeric(8); // "Kj3mX9pL"
16
+ alphanumeric(8, 'seed'); // Reproducible result
17
+ ```
18
+
19
+ ---
20
+
21
+ ### `between(items, seed?)`
22
+ Pick 2 unique items from array. Useful for matchmaking/pairings.
23
+
24
+ ```ts
25
+ between(['a', 'b', 'c', 'd']); // ['b', 'd']
26
+ ```
27
+
28
+ ---
29
+
30
+ ### `coinflip(seed?)`
31
+ 50/50 true/false.
32
+
33
+ ```ts
34
+ coinflip(); // true or false
35
+ coinflip('seed'); // Reproducible result
36
+ ```
37
+
38
+ ---
39
+
40
+ ### `exponential(lambda, seed?)`
41
+ Exponential distribution. Useful for time-between-events simulation.
42
+
43
+ ```ts
44
+ exponential(1.5); // 0.234...
45
+ ```
46
+
47
+ ---
48
+
49
+ ### `gaussian(mean, stddev, seed?)`
50
+ Normal/Gaussian distribution. Useful for realistic simulations.
51
+
52
+ ```ts
53
+ gaussian(100, 15); // ~100 with stddev 15
54
+ ```
55
+
56
+ ---
57
+
58
+ ### `groups(items, total, seed?)`
59
+ Randomly split array into N groups.
60
+
61
+ ```ts
62
+ groups([1, 2, 3, 4, 5, 6], 2); // [[3, 1, 5], [6, 2, 4]]
63
+ ```
64
+
65
+ ---
66
+
67
+ ### `hex(length, seed?)`
68
+ Generate random hex string.
69
+
70
+ ```ts
71
+ hex(16); // "a3f2b1c9e4d8f0a2"
72
+ ```
73
+
74
+ ---
75
+
76
+ ### `item(map, seed?)`
77
+ Weighted random selection from Map.
78
+
79
+ ```ts
80
+ let rewards = new Map([
81
+ ['common', 70],
82
+ ['rare', 25],
83
+ ['legendary', 5]
84
+ ]);
85
+
86
+ item(rewards); // 'common' (70% chance)
87
+ ```
88
+
89
+ ---
90
+
91
+ ### `pick(items, count, seed?)`
92
+ Pick N items with replacement (duplicates allowed).
93
+
94
+ ```ts
95
+ pick([1, 2, 3, 4, 5], 3); // [2, 2, 5] (same item can repeat)
96
+ ```
97
+
98
+ ---
99
+
100
+ ### `range(min, max, integer?, seed?)`
101
+ Random number in range. Set `integer=true` for whole numbers.
102
+
103
+ ```ts
104
+ range(0, 100); // 42.7381...
105
+ range(1, 10, true); // 7
106
+ range(1, 10, true, 'seed'); // Reproducible integer
107
+ ```
108
+
109
+ ---
110
+
111
+ ### `roll(numerator, denominator, seed?)`
112
+ Roll "N in D" chance.
113
+
114
+ ```ts
115
+ roll(1, 100); // 1% chance (1 in 100)
116
+ roll(25, 100); // 25% chance
117
+ roll(1, 6); // ~16.7% chance (1 in 6)
118
+ ```
119
+
120
+ ---
121
+
122
+ ### `sample(items, count, seed?)`
123
+ Pick N unique items (no duplicates).
124
+
125
+ ```ts
126
+ sample([1, 2, 3, 4, 5], 3); // [4, 1, 5] (each item appears once max)
127
+ ```
128
+
129
+ ---
130
+
131
+ ### `shuffle(values, seed?)`
132
+ Fisher-Yates shuffle. Mutates original array.
133
+
134
+ ```ts
135
+ shuffle([1, 2, 3, 4, 5]); // [3, 1, 5, 2, 4]
136
+ ```
137
+
138
+ ---
139
+
140
+ ### `uuid(seed?)`
141
+ Generate UUID v4.
142
+
143
+ ```ts
144
+ uuid(); // "550e8400-e29b-41d4-a716-446655440000"
145
+ ```
146
+
147
+ ---
148
+
149
+ ## Provably Fair / Seeded Mode
150
+
151
+ ---
152
+
153
+ ### rng
154
+
155
+ Cryptographically secure random number generator with optional seeded/provably fair mode.
156
+
157
+ ```ts
158
+ rng(): number // Crypto RNG (0-1)
159
+ rng(seed: string): number // Seeded RNG (deterministic)
160
+ ```
161
+
162
+ ```ts
163
+ import { rng } from '@esportsplus/crypto';
164
+
165
+ // Cryptographic random
166
+ let random = rng(); // 0.7234...
167
+
168
+ // Seeded/provably fair (same seed = same result)
169
+ let seeded = rng('server-seed:client-seed:nonce'); // Deterministic
170
+ ```
171
+
172
+ #### Provably Fair Item Drop Example
173
+
174
+ ```ts
175
+ import { hash, rng } from '@esportsplus/crypto';
176
+
177
+ // 1. Server generates secret seed (hidden until round ends)
178
+ let serverSeed = rng.seed();
179
+ let serverSeedHash = await hash(serverSeed); // Reveal this to client before roll
180
+
181
+ // 2. Client provides their seed
182
+ let clientSeed = 'my-lucky-seed-123';
183
+
184
+ // 3. Combine seeds with nonce for deterministic result
185
+ let nonce = 0; // Increment per roll
186
+ let combinedSeed = `${serverSeed}:${clientSeed}:${nonce}`;
187
+
188
+ // 4. Generate roll and determine drop
189
+ let items = [
190
+ { name: 'Common Sword', weight: 0.70 },
191
+ { name: 'Rare Armor', weight: 0.25 },
192
+ { name: 'Legendary Helm', weight: 0.05 }
193
+ ];
194
+
195
+ let roll = rng(combinedSeed); // 0.0 - 1.0
196
+ let cumulative = 0;
197
+ let drop = items[0];
198
+
199
+ for (let i = 0, n = items.length; i < n; i++) {
200
+ cumulative += items[i].weight;
201
+ if (roll < cumulative) {
202
+ drop = items[i];
203
+ break;
204
+ }
205
+ }
206
+
207
+ // 5. After round: reveal serverSeed so client can verify
208
+ // Client checks: hash(serverSeed) === serverSeedHash
209
+ // Client recalculates: rng(`${serverSeed}:${clientSeed}:${nonce}`) === roll
210
+ ```
211
+
212
+ ---
213
+
214
+ ### rng.seed
215
+
216
+ Generate a cryptographically secure 32-byte hex seed.
217
+
218
+ ```ts
219
+ rng.seed(): string
220
+ ```
221
+
222
+ **Returns:** 64-character hex string
223
+
224
+ ```ts
225
+ import { rng } from '@esportsplus/crypto';
226
+
227
+ let serverSeed = rng.seed(); // 'a1b2c3d4...' (64 chars)
228
+ let clientSeed = rng.seed();
229
+
230
+ // Combine for provably fair result
231
+ let result = rng(`${serverSeed}:${clientSeed}:0`);
232
+ ```
233
+
234
+ #### Provably Fair System - Complete Flow
235
+
236
+ **Database Schema (per user):**
237
+ ```ts
238
+ interface UserSeeds {
239
+ id: string;
240
+ serverSeed: string; // Current secret (never exposed until rotation)
241
+ serverSeedHash: string; // SHA-256 of serverSeed (shown to user)
242
+ clientSeed: string; // User-provided or auto-generated
243
+ nonce: number; // Roll counter, increments each roll
244
+ }
245
+ ```
246
+
247
+ **1. Initial Setup (User Registration/First Visit)**
248
+ ```ts
249
+ import { hash, rng } from '@esportsplus/crypto';
250
+
251
+ // Server generates seeds
252
+ let serverSeed = rng.seed();
253
+ let serverSeedHash = await hash(serverSeed);
254
+ let clientSeed = rng.seed(); // Or user provides custom value
255
+
256
+ // Store in database
257
+ await db.user.create({
258
+ id: odx(),
259
+ serverSeed, // Hidden from user
260
+ serverSeedHash, // Visible to user
261
+ clientSeed, // Visible to user
262
+ nonce: 0
263
+ });
264
+
265
+ // Send to client (NEVER send serverSeed)
266
+ return { serverSeedHash, clientSeed, nonce: 0 };
267
+ ```
268
+
269
+ **2. Rolling (Each Game Action)**
270
+ ```ts
271
+ // Server-side roll
272
+ async function roll(userId: string) {
273
+ let user = await db.user.get(userId);
274
+ let { serverSeed, clientSeed, nonce } = user;
275
+
276
+ // Generate deterministic result
277
+ let combinedSeed = `${serverSeed}:${clientSeed}:${nonce}`;
278
+ let result = rng(combinedSeed);
279
+
280
+ // Increment nonce for next roll
281
+ await db.user.update(userId, { nonce: nonce + 1 });
282
+
283
+ // Return result (NOT the serverSeed)
284
+ return {
285
+ result,
286
+ nonce, // So user can verify later
287
+ clientSeed, // Confirm which seed was used
288
+ serverSeedHash // Confirm commitment
289
+ };
290
+ }
291
+ ```
292
+
293
+ **3. User Changes Client Seed (Anytime)**
294
+ ```ts
295
+ async function updateClientSeed(userId: string, newClientSeed: string) {
296
+ // User can change client seed freely
297
+ // Does NOT reveal server seed
298
+ // Does NOT reset nonce (same server seed epoch)
299
+ await db.user.update(userId, { clientSeed: newClientSeed });
300
+ }
301
+ ```
302
+
303
+ **4. Seed Rotation (User-Initiated)**
304
+ ```ts
305
+ async function rotateServerSeed(userId: string) {
306
+ let user = await db.user.get(userId);
307
+
308
+ // Generate new server seed
309
+ let newServerSeed = rng.seed();
310
+ let newServerSeedHash = await hash(newServerSeed);
311
+
312
+ // Update database
313
+ await db.user.update(userId, {
314
+ serverSeed: newServerSeed,
315
+ serverSeedHash: newServerSeedHash,
316
+ nonce: 0 // Reset nonce for new seed epoch
317
+ });
318
+
319
+ // NOW reveal the old serverSeed for verification
320
+ return {
321
+ previousServerSeed: user.serverSeed, // User can now verify all past rolls
322
+ previousServerSeedHash: user.serverSeedHash,
323
+ previousNonce: user.nonce, // Total rolls under old seed
324
+ newServerSeedHash, // New commitment
325
+ nonce: 0
326
+ };
327
+ }
328
+ ```
329
+
330
+ **5. Client-Side Verification (After Rotation)**
331
+ ```ts
332
+ import { hash, rng } from '@esportsplus/crypto';
333
+
334
+ async function verifyRolls(
335
+ previousServerSeed: string,
336
+ previousServerSeedHash: string,
337
+ clientSeed: string,
338
+ rolls: { nonce: number; result: number }[]
339
+ ) {
340
+ // Step 1: Verify server didn't change the seed
341
+ let computedHash = await hash(previousServerSeed);
342
+ if (computedHash !== previousServerSeedHash) {
343
+ throw new Error('Server seed does not match committed hash!');
344
+ }
345
+
346
+ // Step 2: Verify each roll result
347
+ for (let i = 0, n = rolls.length; i < n; i++) {
348
+ let { nonce, result } = rolls[i];
349
+ let combinedSeed = `${previousServerSeed}:${clientSeed}:${nonce}`;
350
+ let expectedResult = rng(combinedSeed);
351
+
352
+ if (expectedResult !== result) {
353
+ throw new Error(`Roll ${nonce} was manipulated!`);
354
+ }
355
+ }
356
+
357
+ return true; // All rolls verified
358
+ }
359
+ ```
360
+
361
+ **Security Properties:**
362
+ | Property | Guarantee |
363
+ |----------|-----------|
364
+ | Server can't predict | Client seed unknown when server commits |
365
+ | Server can't manipulate | Hash commitment locks server seed |
366
+ | Client can't predict | Server seed hidden until rotation |
367
+ | Deterministic | Same seeds + nonce = same result always |
368
+ | Verifiable | User can audit all rolls after rotation |
369
+
370
+ **Timeline Example:**
371
+ ```
372
+ Day 1: User signs up
373
+ → serverSeed=ABC, hash(ABC)=XYZ shown, clientSeed=123, nonce=0
374
+
375
+ Day 1: User rolls 50 times
376
+ → nonce increments 0→49, serverSeed stays ABC (hidden)
377
+
378
+ Day 3: User changes clientSeed to "lucky-charm"
379
+ → nonce continues at 50, serverSeed stays ABC (hidden)
380
+
381
+ Day 5: User rolls 100 more times
382
+ → nonce increments 50→149
383
+
384
+ Day 7: User clicks "Rotate Seed"
385
+ → ABC revealed, user verifies all 150 rolls
386
+ → New serverSeed=DEF, hash(DEF)=UVW shown, nonce=0
387
+ ```
@@ -0,0 +1,2 @@
1
+ declare const _default: (length: number, seed?: string) => string;
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { rng } from './rng.js';
2
+ const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
3
+ export default (length, seed) => {
4
+ if (length <= 0) {
5
+ throw new Error('@esportsplus/random: length must be positive');
6
+ }
7
+ let n = CHARS.length, result = '';
8
+ for (let i = 0; i < length; i++) {
9
+ result += CHARS[(rng(seed) * n) >>> 0];
10
+ }
11
+ return result;
12
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: <T>(items: T[], seed?: string) => [T, T];
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { rng } from './rng.js';
2
+ export default (items, seed) => {
3
+ let n = items.length;
4
+ if (n < 2) {
5
+ throw new Error('@esportsplus/random: need at least 2 items');
6
+ }
7
+ let first = (rng(seed) * n) >>> 0, second = (rng(seed) * (n - 1)) >>> 0;
8
+ if (second >= first) {
9
+ second++;
10
+ }
11
+ return [items[first], items[second]];
12
+ };
@@ -1,2 +1,2 @@
1
- declare const _default: () => boolean;
1
+ declare const _default: (seed?: string) => boolean;
2
2
  export default _default;
package/build/coinflip.js CHANGED
@@ -1,4 +1,4 @@
1
- import { rng } from '@esportsplus/crypto';
2
- export default () => {
3
- return rng() < 0.5;
1
+ import { rng } from './rng.js';
2
+ export default (seed) => {
3
+ return rng(seed) < 0.5;
4
4
  };
@@ -0,0 +1,2 @@
1
+ declare const _default: (lambda: number, seed?: string) => number;
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import { rng } from './rng.js';
2
+ export default (lambda, seed) => {
3
+ if (lambda <= 0) {
4
+ throw new Error('@esportsplus/random: lambda must be positive');
5
+ }
6
+ let u = rng(seed);
7
+ while (u === 0) {
8
+ u = rng(seed);
9
+ }
10
+ return -Math.log(u) / lambda;
11
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: (mean: number, stddev: number, seed?: string) => number;
2
+ export default _default;
@@ -0,0 +1,12 @@
1
+ import { rng } from './rng.js';
2
+ export default (mean, stddev, seed) => {
3
+ if (stddev < 0) {
4
+ throw new Error('@esportsplus/random: stddev cannot be negative');
5
+ }
6
+ let u1 = rng(seed), u2 = rng(seed);
7
+ while (u1 === 0) {
8
+ u1 = rng(seed);
9
+ }
10
+ let z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
11
+ return mean + stddev * z;
12
+ };
@@ -0,0 +1,2 @@
1
+ declare const _default: <T>(items: T[], total: number, seed?: string) => T[][];
2
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import shuffle from './shuffle.js';
2
+ export default (items, total, seed) => {
3
+ if (total <= 0) {
4
+ throw new Error('@esportsplus/random: total must be positive');
5
+ }
6
+ let copy = shuffle([...items], seed), groups = [], n = copy.length, size = Math.ceil(n / total);
7
+ for (let i = 0; i < total; i++) {
8
+ groups.push(copy.slice(i * size, (i + 1) * size));
9
+ }
10
+ return groups;
11
+ };
package/build/hex.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ declare const _default: (length: number, seed?: string) => string;
2
+ export default _default;
package/build/hex.js ADDED
@@ -0,0 +1,12 @@
1
+ import { rng } from './rng.js';
2
+ const CHARS = '0123456789abcdef';
3
+ export default (length, seed) => {
4
+ if (length <= 0) {
5
+ throw new Error('@esportsplus/random: length must be positive');
6
+ }
7
+ let result = '';
8
+ for (let i = 0; i < length; i++) {
9
+ result += CHARS[(rng(seed) * 16) >>> 0];
10
+ }
11
+ return result;
12
+ };
package/build/index.d.ts CHANGED
@@ -1,6 +1,14 @@
1
+ import alphanumeric from './alphanumeric.js';
2
+ import between from './between.js';
1
3
  import coinflip from './coinflip.js';
4
+ import exponential from './exponential.js';
5
+ import gaussian from './gaussian.js';
6
+ import groups from './groups.js';
7
+ import hex from './hex.js';
2
8
  import item from './item.js';
9
+ import pick from './pick.js';
3
10
  import range from './range.js';
4
11
  import roll from './roll.js';
12
+ import sample from './sample.js';
5
13
  import shuffle from './shuffle.js';
6
- export { coinflip, item, range, roll, shuffle };
14
+ export { alphanumeric, between, coinflip, exponential, gaussian, groups, hex, item, pick, range, roll, sample, shuffle };
package/build/index.js CHANGED
@@ -1,6 +1,14 @@
1
+ import alphanumeric from './alphanumeric.js';
2
+ import between from './between.js';
1
3
  import coinflip from './coinflip.js';
4
+ import exponential from './exponential.js';
5
+ import gaussian from './gaussian.js';
6
+ import groups from './groups.js';
7
+ import hex from './hex.js';
2
8
  import item from './item.js';
9
+ import pick from './pick.js';
3
10
  import range from './range.js';
4
11
  import roll from './roll.js';
12
+ import sample from './sample.js';
5
13
  import shuffle from './shuffle.js';
6
- export { coinflip, item, range, roll, shuffle };
14
+ export { alphanumeric, between, coinflip, exponential, gaussian, groups, hex, item, pick, range, roll, sample, shuffle };
package/build/item.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: <T>(items: T[], weights?: number[]) => T;
1
+ declare const _default: <T>(map: Map<T, number>, seed?: string) => T;
2
2
  export default _default;
package/build/item.js CHANGED
@@ -1,22 +1,18 @@
1
- import { rng } from '@esportsplus/crypto';
2
- export default (items, weights) => {
3
- if (weights === undefined) {
4
- return items[Math.floor(rng() * items.length)];
1
+ import { rng } from './rng.js';
2
+ export default (map, seed) => {
3
+ if (map.size === 0) {
4
+ throw new Error('@esportsplus/random: map cannot be empty');
5
5
  }
6
- let n = items.length, random = 0;
7
- if (n !== weights.length) {
8
- throw new Error('Random: each item requires a weight');
6
+ let current = 0, total = 0;
7
+ for (let weight of map.values()) {
8
+ total += weight;
9
9
  }
10
- for (let i = 0; i < n; i++) {
11
- random += weights[i];
12
- }
13
- let current = 0;
14
- random *= rng();
15
- for (let i = 0; i < n; i++) {
16
- current += weights[i];
10
+ let random = rng(seed) * total;
11
+ for (let [item, weight] of map) {
12
+ current += weight;
17
13
  if (random <= current) {
18
- return items[i];
14
+ return item;
19
15
  }
20
16
  }
21
- throw new Error('Random: weighted item pick failed');
17
+ throw new Error('@esportsplus/random: weighted item pick failed');
22
18
  };
@@ -0,0 +1,2 @@
1
+ declare const _default: <T>(items: T[], count: number, seed?: string) => T[];
2
+ export default _default;
package/build/pick.js ADDED
@@ -0,0 +1,14 @@
1
+ import { rng } from './rng.js';
2
+ export default (items, count, seed) => {
3
+ if (items.length === 0) {
4
+ throw new Error('@esportsplus/random: items cannot be empty');
5
+ }
6
+ if (count <= 0) {
7
+ throw new Error('@esportsplus/random: count must be positive');
8
+ }
9
+ let n = items.length, result = [];
10
+ for (let i = 0; i < count; i++) {
11
+ result.push(items[(rng(seed) * n) >>> 0]);
12
+ }
13
+ return result;
14
+ };
package/build/range.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: (min: number, max: number, integer?: boolean) => number;
1
+ declare const _default: (min: number, max: number, integer?: boolean, seed?: string) => number;
2
2
  export default _default;
package/build/range.js CHANGED
@@ -1,9 +1,12 @@
1
- import { rng } from '@esportsplus/crypto';
2
- export default (min, max, integer = false) => {
1
+ import { rng } from './rng.js';
2
+ export default (min, max, integer = false, seed) => {
3
+ if (min > max) {
4
+ throw new Error('@esportsplus/random: min cannot be greater than max');
5
+ }
3
6
  if (!integer) {
4
- return rng() * (max - min) + min;
7
+ return rng(seed) * (max - min) + min;
5
8
  }
6
9
  min = Math.ceil(min);
7
10
  max = Math.floor(max) + 1;
8
- return Math.floor(rng() * (max - min) + min);
11
+ return Math.floor(rng(seed) * (max - min) + min);
9
12
  };
package/build/rng.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ declare function rng(seed?: string): number;
2
+ declare namespace rng {
3
+ var seed: () => string;
4
+ }
5
+ export { rng };
package/build/rng.js ADDED
@@ -0,0 +1,53 @@
1
+ const MAX = 0xFFFFFFFF;
2
+ const RNG_BUFFER = new Uint32Array(1);
3
+ const SEED_BUFFER = new Uint8Array(32);
4
+ function cyrb128(str) {
5
+ let h1 = 1779033703, h2 = 3144134277, h3 = 1013904242, h4 = 2773480762;
6
+ for (let i = 0, n = str.length; i < n; i++) {
7
+ let k = str.charCodeAt(i);
8
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
9
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
10
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
11
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
12
+ }
13
+ h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
14
+ h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
15
+ h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
16
+ h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
17
+ h1 ^= (h2 ^ h3 ^ h4);
18
+ h2 ^= h1;
19
+ h3 ^= h1;
20
+ h4 ^= h1;
21
+ return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0];
22
+ }
23
+ function sfc32(a, b, c, d) {
24
+ return () => {
25
+ a >>>= 0;
26
+ b >>>= 0;
27
+ c >>>= 0;
28
+ d >>>= 0;
29
+ let t = (a + b) | 0;
30
+ a = b ^ b >>> 9;
31
+ b = c + (c << 3) | 0;
32
+ c = (c << 21 | c >>> 11);
33
+ d = d + 1 | 0;
34
+ t = t + d | 0;
35
+ c = c + t | 0;
36
+ return (t >>> 0) / MAX;
37
+ };
38
+ }
39
+ function rng(seed) {
40
+ if (seed) {
41
+ let [a, b, c, d] = cyrb128(seed);
42
+ return sfc32(a, b, c, d)();
43
+ }
44
+ return crypto.getRandomValues(RNG_BUFFER)[0] / MAX;
45
+ }
46
+ rng.seed = () => {
47
+ let bytes = crypto.getRandomValues(SEED_BUFFER), hex = '';
48
+ for (let i = 0, n = bytes.length; i < n; i++) {
49
+ hex += bytes[i].toString(16).padStart(2, '0');
50
+ }
51
+ return hex;
52
+ };
53
+ export { rng };
package/build/roll.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- declare const _default: (percentage: number) => boolean;
1
+ declare const _default: (numerator: number, denominator: number, seed?: string) => boolean;
2
2
  export default _default;
package/build/roll.js CHANGED
@@ -1,4 +1,7 @@
1
- import { rng } from '@esportsplus/crypto';
2
- export default (percentage) => {
3
- return rng() <= percentage;
1
+ import { rng } from './rng.js';
2
+ export default (numerator, denominator, seed) => {
3
+ if (numerator <= 0 || denominator <= 0) {
4
+ throw new Error('@esportsplus/random: numerator and denominator must be positive');
5
+ }
6
+ return rng(seed) <= (numerator / denominator);
4
7
  };
@@ -0,0 +1,2 @@
1
+ declare const _default: <T>(items: T[], count: number, seed?: string) => T[];
2
+ export default _default;
@@ -0,0 +1,14 @@
1
+ import shuffle from './shuffle.js';
2
+ export default (items, count, seed) => {
3
+ let n = items.length;
4
+ if (n === 0) {
5
+ throw new Error('@esportsplus/random: items cannot be empty');
6
+ }
7
+ if (count <= 0) {
8
+ throw new Error('@esportsplus/random: count must be positive');
9
+ }
10
+ if (count > n) {
11
+ throw new Error('@esportsplus/random: count cannot exceed items length');
12
+ }
13
+ return shuffle([...items], seed).slice(0, count);
14
+ };
@@ -1,2 +1,2 @@
1
- declare const _default: (values: any[]) => any[];
1
+ declare const _default: <T>(values: T[], seed?: string) => T[];
2
2
  export default _default;
package/build/shuffle.js CHANGED
@@ -1,8 +1,11 @@
1
- import { rng } from '@esportsplus/crypto';
2
- export default (values) => {
1
+ import { rng } from './rng.js';
2
+ export default (values, seed) => {
3
+ if (values.length === 0) {
4
+ return values;
5
+ }
3
6
  let n = values.length, random, value;
4
7
  while (--n > 0) {
5
- random = Math.floor(rng() * (n + 1));
8
+ random = (rng(seed) * (n + 1)) >>> 0;
6
9
  value = values[random];
7
10
  values[random] = values[n];
8
11
  values[n] = value;
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "author": "ICJR",
3
- "dependencies": {
4
- "@esportsplus/crypto": "^0.1.0"
5
- },
6
3
  "devDependencies": {
7
- "@esportsplus/typescript": "^0.9.1"
4
+ "@esportsplus/typescript": "^0.9.2"
8
5
  },
9
6
  "main": "build/index.js",
10
7
  "name": "@esportsplus/random",
11
8
  "private": false,
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/esportsplus/random"
12
+ },
12
13
  "type": "module",
13
14
  "types": "build/index.d.ts",
14
- "version": "0.0.28",
15
+ "version": "0.0.30",
15
16
  "scripts": {
16
17
  "build": "tsc && tsc-alias",
17
18
  "-": "-"
@@ -0,0 +1,20 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ const CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
5
+
6
+
7
+ export default (length: number, seed?: string) => {
8
+ if (length <= 0) {
9
+ throw new Error('@esportsplus/random: length must be positive');
10
+ }
11
+
12
+ let n = CHARS.length,
13
+ result = '';
14
+
15
+ for (let i = 0; i < length; i++) {
16
+ result += CHARS[(rng(seed) * n) >>> 0];
17
+ }
18
+
19
+ return result;
20
+ };
package/src/between.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ export default <T>(items: T[], seed?: string): [T, T] => {
5
+ let n = items.length;
6
+
7
+ if (n < 2) {
8
+ throw new Error('@esportsplus/random: need at least 2 items');
9
+ }
10
+
11
+ let first = (rng(seed) * n) >>> 0,
12
+ second = (rng(seed) * (n - 1)) >>> 0;
13
+
14
+ if (second >= first) {
15
+ second++;
16
+ }
17
+
18
+ return [items[first], items[second]];
19
+ };
package/src/coinflip.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { rng } from '@esportsplus/crypto';
1
+ import { rng } from './rng';
2
2
 
3
3
 
4
- export default () => {
5
- return rng() < 0.5;
4
+ export default (seed?: string) => {
5
+ return rng(seed) < 0.5;
6
6
  };
@@ -0,0 +1,19 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ // Box-Muller transform for normal distribution
5
+ // - https://en.wikipedia.org/wiki/Box-Muller_transform
6
+ export default (lambda: number, seed?: string) => {
7
+ if (lambda <= 0) {
8
+ throw new Error('@esportsplus/random: lambda must be positive');
9
+ }
10
+
11
+ let u = rng(seed);
12
+
13
+ // Avoid log(0)
14
+ while (u === 0) {
15
+ u = rng(seed);
16
+ }
17
+
18
+ return -Math.log(u) / lambda;
19
+ };
@@ -0,0 +1,22 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ // Box-Muller transform for normal distribution
5
+ // - https://en.wikipedia.org/wiki/Box-Muller_transform
6
+ export default (mean: number, stddev: number, seed?: string) => {
7
+ if (stddev < 0) {
8
+ throw new Error('@esportsplus/random: stddev cannot be negative');
9
+ }
10
+
11
+ let u1 = rng(seed),
12
+ u2 = rng(seed);
13
+
14
+ // Avoid log(0)
15
+ while (u1 === 0) {
16
+ u1 = rng(seed);
17
+ }
18
+
19
+ let z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
20
+
21
+ return mean + stddev * z;
22
+ };
package/src/groups.ts ADDED
@@ -0,0 +1,19 @@
1
+ import shuffle from './shuffle';
2
+
3
+
4
+ export default <T>(items: T[], total: number, seed?: string): T[][] => {
5
+ if (total <= 0) {
6
+ throw new Error('@esportsplus/random: total must be positive');
7
+ }
8
+
9
+ let copy = shuffle([...items], seed),
10
+ groups: T[][] = [],
11
+ n = copy.length,
12
+ size = Math.ceil(n / total);
13
+
14
+ for (let i = 0; i < total; i++) {
15
+ groups.push(copy.slice(i * size, (i + 1) * size));
16
+ }
17
+
18
+ return groups;
19
+ };
package/src/hex.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ const CHARS = '0123456789abcdef';
5
+
6
+
7
+ export default (length: number, seed?: string) => {
8
+ if (length <= 0) {
9
+ throw new Error('@esportsplus/random: length must be positive');
10
+ }
11
+
12
+ let result = '';
13
+
14
+ for (let i = 0; i < length; i++) {
15
+ result += CHARS[(rng(seed) * 16) >>> 0];
16
+ }
17
+
18
+ return result;
19
+ };
package/src/index.ts CHANGED
@@ -1,8 +1,16 @@
1
+ import alphanumeric from './alphanumeric';
2
+ import between from './between';
1
3
  import coinflip from './coinflip';
4
+ import exponential from './exponential';
5
+ import gaussian from './gaussian';
6
+ import groups from './groups';
7
+ import hex from './hex';
2
8
  import item from './item';
9
+ import pick from './pick';
3
10
  import range from './range';
4
11
  import roll from './roll';
12
+ import sample from './sample';
5
13
  import shuffle from './shuffle';
6
14
 
7
15
 
8
- export { coinflip, item, range, roll, shuffle };
16
+ export { alphanumeric, between, coinflip, exponential, gaussian, groups, hex, item, pick, range, roll, sample, shuffle };
package/src/item.ts CHANGED
@@ -1,33 +1,27 @@
1
- import { rng } from '@esportsplus/crypto';
1
+ import { rng } from './rng';
2
2
 
3
3
 
4
- export default <T>(items: T[], weights?: number[]): T => {
5
- if (weights === undefined) {
6
- return items[ Math.floor(rng() * items.length) ];
4
+ export default <T>(map: Map<T, number>, seed?: string): T => {
5
+ if (map.size === 0) {
6
+ throw new Error('@esportsplus/random: map cannot be empty');
7
7
  }
8
8
 
9
- let n = items.length,
10
- random = 0;
9
+ let current = 0,
10
+ total = 0;
11
11
 
12
- if (n !== weights.length) {
13
- throw new Error('Random: each item requires a weight');
12
+ for (let weight of map.values()) {
13
+ total += weight;
14
14
  }
15
15
 
16
- for (let i = 0; i < n; i++) {
17
- random += weights[i];
18
- }
19
-
20
- let current = 0;
21
-
22
- random *= rng();
16
+ let random = rng(seed) * total;
23
17
 
24
- for (let i = 0; i < n; i++) {
25
- current += weights[i];
18
+ for (let [item, weight] of map) {
19
+ current += weight;
26
20
 
27
21
  if (random <= current) {
28
- return items[i];
22
+ return item;
29
23
  }
30
24
  }
31
25
 
32
- throw new Error('Random: weighted item pick failed');
26
+ throw new Error('@esportsplus/random: weighted item pick failed');
33
27
  };
package/src/pick.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { rng } from './rng';
2
+
3
+
4
+ export default <T>(items: T[], count: number, seed?: string): T[] => {
5
+ if (items.length === 0) {
6
+ throw new Error('@esportsplus/random: items cannot be empty');
7
+ }
8
+
9
+ if (count <= 0) {
10
+ throw new Error('@esportsplus/random: count must be positive');
11
+ }
12
+
13
+ let n = items.length,
14
+ result: T[] = [];
15
+
16
+ for (let i = 0; i < count; i++) {
17
+ result.push(items[(rng(seed) * n) >>> 0]);
18
+ }
19
+
20
+ return result;
21
+ };
package/src/range.ts CHANGED
@@ -1,13 +1,17 @@
1
- import { rng } from '@esportsplus/crypto';
1
+ import { rng } from './rng';
2
2
 
3
3
 
4
- export default (min: number, max: number, integer = false) => {
4
+ export default (min: number, max: number, integer = false, seed?: string) => {
5
+ if (min > max) {
6
+ throw new Error('@esportsplus/random: min cannot be greater than max');
7
+ }
8
+
5
9
  if (!integer) {
6
- return rng() * (max - min) + min;
10
+ return rng(seed) * (max - min) + min;
7
11
  }
8
12
 
9
13
  min = Math.ceil(min);
10
14
  max = Math.floor(max) + 1;
11
15
 
12
- return Math.floor(rng() * (max - min) + min);
16
+ return Math.floor(rng(seed) * (max - min) + min);
13
17
  };
package/src/rng.ts ADDED
@@ -0,0 +1,79 @@
1
+ const MAX = 0xFFFFFFFF;
2
+
3
+ const RNG_BUFFER = new Uint32Array(1);
4
+
5
+ const SEED_BUFFER = new Uint8Array(32);
6
+
7
+
8
+ function cyrb128(str: string): [number, number, number, number] {
9
+ let h1 = 1779033703,
10
+ h2 = 3144134277,
11
+ h3 = 1013904242,
12
+ h4 = 2773480762;
13
+
14
+ for (let i = 0, n = str.length; i < n; i++) {
15
+ let k = str.charCodeAt(i);
16
+
17
+ h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
18
+ h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
19
+ h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
20
+ h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
21
+ }
22
+
23
+ h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
24
+ h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
25
+ h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
26
+ h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
27
+
28
+ h1 ^= (h2 ^ h3 ^ h4);
29
+ h2 ^= h1;
30
+ h3 ^= h1;
31
+ h4 ^= h1;
32
+
33
+ return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0];
34
+ }
35
+
36
+ function sfc32(a: number, b: number, c: number, d: number): () => number {
37
+ return () => {
38
+ a >>>= 0;
39
+ b >>>= 0;
40
+ c >>>= 0;
41
+ d >>>= 0;
42
+
43
+ let t = (a + b) | 0;
44
+
45
+ a = b ^ b >>> 9;
46
+ b = c + (c << 3) | 0;
47
+ c = (c << 21 | c >>> 11);
48
+ d = d + 1 | 0;
49
+ t = t + d | 0;
50
+ c = c + t | 0;
51
+
52
+ return (t >>> 0) / MAX;
53
+ };
54
+ }
55
+
56
+
57
+ function rng(seed?: string): number {
58
+ if (seed) {
59
+ let [a, b, c, d] = cyrb128(seed);
60
+
61
+ return sfc32(a, b, c, d)();
62
+ }
63
+
64
+ return crypto.getRandomValues(RNG_BUFFER)[0] / MAX;
65
+ }
66
+
67
+ rng.seed = () => {
68
+ let bytes = crypto.getRandomValues(SEED_BUFFER),
69
+ hex = '';
70
+
71
+ for (let i = 0, n = bytes.length; i < n; i++) {
72
+ hex += bytes[i].toString(16).padStart(2, '0');
73
+ }
74
+
75
+ return hex;
76
+ };
77
+
78
+
79
+ export { rng };
package/src/roll.ts CHANGED
@@ -1,6 +1,10 @@
1
- import { rng } from '@esportsplus/crypto';
1
+ import { rng } from './rng';
2
2
 
3
3
 
4
- export default (percentage: number) => {
5
- return rng() <= percentage;
4
+ export default (numerator: number, denominator: number, seed?: string) => {
5
+ if (numerator <= 0 || denominator <= 0) {
6
+ throw new Error('@esportsplus/random: numerator and denominator must be positive');
7
+ }
8
+
9
+ return rng(seed) <= (numerator / denominator);
6
10
  };
package/src/sample.ts ADDED
@@ -0,0 +1,20 @@
1
+ import shuffle from './shuffle';
2
+
3
+
4
+ export default <T>(items: T[], count: number, seed?: string): T[] => {
5
+ let n = items.length;
6
+
7
+ if (n === 0) {
8
+ throw new Error('@esportsplus/random: items cannot be empty');
9
+ }
10
+
11
+ if (count <= 0) {
12
+ throw new Error('@esportsplus/random: count must be positive');
13
+ }
14
+
15
+ if (count > n) {
16
+ throw new Error('@esportsplus/random: count cannot exceed items length');
17
+ }
18
+
19
+ return shuffle([...items], seed).slice(0, count);
20
+ };
package/src/shuffle.ts CHANGED
@@ -1,15 +1,19 @@
1
- import { rng } from '@esportsplus/crypto';
1
+ import { rng } from './rng';
2
2
 
3
3
 
4
4
  // Fisher-Yates shuffle
5
5
  // - https://wikipedia.org/wiki/Fisher-Yates_shuffle
6
- export default (values: any[]) => {
6
+ export default <T>(values: T[], seed?: string): T[] => {
7
+ if (values.length === 0) {
8
+ return values;
9
+ }
10
+
7
11
  let n = values.length,
8
12
  random: number,
9
- value;
13
+ value: T;
10
14
 
11
15
  while (--n > 0) {
12
- random = Math.floor(rng() * (n + 1));
16
+ random = (rng(seed) * (n + 1)) >>> 0;
13
17
  value = values[random];
14
18
 
15
19
  values[random] = values[n];