@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.
- package/.github/dependabot.yml +2 -0
- package/.github/workflows/bump.yml +2 -0
- package/.github/workflows/dependabot.yml +12 -0
- package/.github/workflows/publish.yml +4 -2
- package/README.md +387 -0
- package/build/alphanumeric.d.ts +2 -0
- package/build/alphanumeric.js +12 -0
- package/build/between.d.ts +2 -0
- package/build/between.js +12 -0
- package/build/coinflip.d.ts +1 -1
- package/build/coinflip.js +3 -3
- package/build/exponential.d.ts +2 -0
- package/build/exponential.js +11 -0
- package/build/gaussian.d.ts +2 -0
- package/build/gaussian.js +12 -0
- package/build/groups.d.ts +2 -0
- package/build/groups.js +11 -0
- package/build/hex.d.ts +2 -0
- package/build/hex.js +12 -0
- package/build/index.d.ts +9 -1
- package/build/index.js +9 -1
- package/build/item.d.ts +1 -1
- package/build/item.js +12 -16
- package/build/pick.d.ts +2 -0
- package/build/pick.js +14 -0
- package/build/range.d.ts +1 -1
- package/build/range.js +7 -4
- package/build/rng.d.ts +5 -0
- package/build/rng.js +53 -0
- package/build/roll.d.ts +1 -1
- package/build/roll.js +6 -3
- package/build/sample.d.ts +2 -0
- package/build/sample.js +14 -0
- package/build/shuffle.d.ts +1 -1
- package/build/shuffle.js +6 -3
- package/package.json +6 -5
- package/src/alphanumeric.ts +20 -0
- package/src/between.ts +19 -0
- package/src/coinflip.ts +3 -3
- package/src/exponential.ts +19 -0
- package/src/gaussian.ts +22 -0
- package/src/groups.ts +19 -0
- package/src/hex.ts +19 -0
- package/src/index.ts +9 -1
- package/src/item.ts +13 -19
- package/src/pick.ts +21 -0
- package/src/range.ts +8 -4
- package/src/rng.ts +79 -0
- package/src/roll.ts +7 -3
- package/src/sample.ts +20 -0
- package/src/shuffle.ts +8 -4
package/.github/dependabot.yml
CHANGED
|
@@ -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,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
|
-
|
|
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,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
|
+
};
|
package/build/between.js
ADDED
|
@@ -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
|
+
};
|
package/build/coinflip.d.ts
CHANGED
|
@@ -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 '
|
|
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,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,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
|
+
};
|
package/build/groups.js
ADDED
|
@@ -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
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>(
|
|
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 '
|
|
2
|
-
export default (
|
|
3
|
-
if (
|
|
4
|
-
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
let current = 0, total = 0;
|
|
7
|
+
for (let weight of map.values()) {
|
|
8
|
+
total += weight;
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
14
|
+
return item;
|
|
19
15
|
}
|
|
20
16
|
}
|
|
21
|
-
throw new Error('
|
|
17
|
+
throw new Error('@esportsplus/random: weighted item pick failed');
|
|
22
18
|
};
|
package/build/pick.d.ts
ADDED
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 '
|
|
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
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: (
|
|
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 '
|
|
2
|
-
export default (
|
|
3
|
-
|
|
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
|
};
|
package/build/sample.js
ADDED
|
@@ -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
|
+
};
|
package/build/shuffle.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: (values:
|
|
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 '
|
|
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 =
|
|
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.
|
|
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.
|
|
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
|
@@ -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
|
+
};
|
package/src/gaussian.ts
ADDED
|
@@ -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 '
|
|
1
|
+
import { rng } from './rng';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
export default <T>(
|
|
5
|
-
if (
|
|
6
|
-
|
|
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
|
|
10
|
-
|
|
9
|
+
let current = 0,
|
|
10
|
+
total = 0;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
for (let weight of map.values()) {
|
|
13
|
+
total += weight;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
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
|
|
25
|
-
current +=
|
|
18
|
+
for (let [item, weight] of map) {
|
|
19
|
+
current += weight;
|
|
26
20
|
|
|
27
21
|
if (random <= current) {
|
|
28
|
-
return
|
|
22
|
+
return item;
|
|
29
23
|
}
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
throw new Error('
|
|
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 '
|
|
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 '
|
|
1
|
+
import { rng } from './rng';
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
export default (
|
|
5
|
-
|
|
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 '
|
|
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:
|
|
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 =
|
|
16
|
+
random = (rng(seed) * (n + 1)) >>> 0;
|
|
13
17
|
value = values[random];
|
|
14
18
|
|
|
15
19
|
values[random] = values[n];
|