pdrng 1.0.0

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.
@@ -0,0 +1,15 @@
1
+ # Contributing
2
+
3
+ When contributing to this repository, please first discuss the change you wish to make via issue,
4
+ email, or any other method with the owners of this repository before making a change.
5
+
6
+ ## Pull Request Process
7
+
8
+ 1. Ensure any install or build dependencies are removed before the end of the layer when doing a
9
+ build.
10
+ 2. Update the README.md with details of changes to the interface, this includes new environment
11
+ variables, exposed ports, useful file locations and container parameters.
12
+ 3. Increase the version numbers in any examples files and the README.md to the new version that this
13
+ Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/).
14
+ 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you
15
+ do not have permission to do that, you may request the second reviewer to merge it for you.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian Funk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,260 @@
1
+ [![pdrng](https://img.shields.io/badge/pdrng-Deterministic%20RNG-b5d4ff.svg)](https://github.com/brianfunk/pdrng)
2
+ [![npm version](https://img.shields.io/npm/v/pdrng.svg)](https://www.npmjs.com/package/pdrng)
3
+ [![npm downloads](https://img.shields.io/npm/dm/pdrng.svg)](https://www.npmjs.com/package/pdrng)
4
+ [![CI](https://github.com/brianfunk/pdrng/actions/workflows/ci.yml/badge.svg)](https://github.com/brianfunk/pdrng/actions/workflows/ci.yml)
5
+ [![Open Source Love](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/ellerbrock/open-source-badge/)
6
+ [![Semver](https://img.shields.io/badge/SemVer-2.0-blue.svg)](http://semver.org/spec/v2.0.0.html)
7
+ [![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT)
8
+ [![LinkedIn](https://img.shields.io/badge/Linked-In-blue.svg)](https://www.linkedin.com/in/brianrandyfunk)
9
+
10
+ # pdrng
11
+
12
+ > Pseudo Deterministic Random Number Generator — seed-based deterministic number generation.
13
+
14
+ A JavaScript library for generating deterministic outputs based on a numeric seed. Given the same seed, every function produces the same result every time. Useful for testing, simulations, reproducible demos, and seeded content generation.
15
+
16
+ **Default seed: 814**
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install pdrng
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```javascript
27
+ import pdrng from 'pdrng';
28
+
29
+ pdrng() // 814
30
+ pdrng(1) // 8
31
+ pdrng(6) // 814814
32
+ pdrng.coin() // "tails"
33
+ pdrng.dice() // 4
34
+ pdrng.card() // "8 of Diamonds"
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### Core
40
+
41
+ #### `pdrng(digits?, options?)`
42
+
43
+ Generate a deterministic number with the specified number of digits.
44
+
45
+ ```javascript
46
+ pdrng() // 814 (default: 3 digits)
47
+ pdrng(1) // 8
48
+ pdrng(2) // 14
49
+ pdrng(4) // 8148
50
+ pdrng(5) // 81414
51
+ pdrng(6) // 814814
52
+ ```
53
+
54
+ ### Utilities
55
+
56
+ #### `float(precision?, options?)`
57
+
58
+ ```javascript
59
+ float() // 0.814814
60
+ float(3) // 0.814
61
+ ```
62
+
63
+ #### `range(min, max, options?)`
64
+
65
+ ```javascript
66
+ range(1, 100) // 14
67
+ ```
68
+
69
+ #### `array(count, digits?, options?)`
70
+
71
+ ```javascript
72
+ array(3, 2) // [14, 46, 78] (sub-seeds per element)
73
+ ```
74
+
75
+ #### `uuid(options?)`
76
+
77
+ ```javascript
78
+ uuid() // deterministic UUID v4 format
79
+ ```
80
+
81
+ #### `oddOrEven(options?)`
82
+
83
+ ```javascript
84
+ oddOrEven() // "even"
85
+ ```
86
+
87
+ #### `redOrBlack(options?)`
88
+
89
+ ```javascript
90
+ redOrBlack() // "red"
91
+ ```
92
+
93
+ #### `randomSeed()`
94
+
95
+ Generate a random seed using `crypto.getRandomValues` (with `Math.random` fallback).
96
+
97
+ ```javascript
98
+ randomSeed() // e.g. 3847291056 (different each call)
99
+ ```
100
+
101
+ ### Simulation Functions
102
+
103
+ #### `coin(options?)`
104
+
105
+ Deterministic coin flip.
106
+
107
+ ```javascript
108
+ coin() // "tails"
109
+ ```
110
+
111
+ #### `dice(sides?, options?)`
112
+
113
+ Deterministic die result.
114
+
115
+ ```javascript
116
+ dice() // 4 (6-sided)
117
+ dice(20) // 14
118
+ ```
119
+
120
+ #### `card(options?)`
121
+
122
+ Deterministic playing card draw.
123
+
124
+ ```javascript
125
+ card() // "8 of Diamonds"
126
+ ```
127
+
128
+ #### `rps(options?)`
129
+
130
+ Deterministic rock-paper-scissors.
131
+
132
+ ```javascript
133
+ rps() // "scissors"
134
+ ```
135
+
136
+ #### `magic8(options?)`
137
+
138
+ Deterministic 8-ball response.
139
+
140
+ ```javascript
141
+ magic8() // "Reply hazy, try again."
142
+ ```
143
+
144
+ #### `zodiac(options?)`
145
+
146
+ Deterministic zodiac sign.
147
+
148
+ ```javascript
149
+ zodiac() // "Gemini"
150
+ ```
151
+
152
+ #### `tarot(options?)`
153
+
154
+ Deterministic tarot card.
155
+
156
+ ```javascript
157
+ tarot() // "The Magician"
158
+ ```
159
+
160
+ #### `fortune(options?)`
161
+
162
+ Deterministic fortune message.
163
+
164
+ ```javascript
165
+ fortune() // "The answer you seek was never in doubt."
166
+ ```
167
+
168
+ #### `spin(array, options?)`
169
+
170
+ Deterministic selection from an array.
171
+
172
+ ```javascript
173
+ spin(['a', 'b', 'c', 'd']) // "c"
174
+ ```
175
+
176
+ #### `roll(notation, options?)`
177
+
178
+ Deterministic dice notation result (tabletop RPG style).
179
+
180
+ ```javascript
181
+ roll('2d6+3') // { rolls: [5, 1], modifier: 3, total: 9 }
182
+ ```
183
+
184
+ #### `bingo(options?)`
185
+
186
+ Deterministic bingo call.
187
+
188
+ ```javascript
189
+ bingo() // "B-14"
190
+ ```
191
+
192
+ #### `color(options?)`
193
+
194
+ Deterministic hex color.
195
+
196
+ ```javascript
197
+ color() // "#a81414"
198
+ ```
199
+
200
+ #### `roulette(options?)`
201
+
202
+ Deterministic number with color and parity.
203
+
204
+ ```javascript
205
+ roulette() // { number: 14, color: "red", parity: "even" }
206
+ ```
207
+
208
+ ## Custom Seeds
209
+
210
+ Every function accepts an `options` object with a `seed` property:
211
+
212
+ ```javascript
213
+ import { coin, dice, card } from 'pdrng';
214
+
215
+ coin({ seed: 42 }) // "heads"
216
+ dice(6, { seed: 42 }) // 4
217
+ card({ seed: 'brian' }) // deterministic card from text seed
218
+ ```
219
+
220
+ Text seeds are converted using a rolling XOR hash, designed so that `"brian"` maps to seed 814:
221
+
222
+ ```javascript
223
+ pdrng(3, { seed: 'brian' }) // 814 (same as default!)
224
+ pdrng(4, { seed: 'brian' }) // 8148
225
+ ```
226
+
227
+ ## Random Seeds
228
+
229
+ While pdrng is deterministic by design, you can use random input to get non-deterministic behavior. Pass `Math.random()`, `crypto.getRandomValues()`, or use the built-in `randomSeed()` helper:
230
+
231
+ ```javascript
232
+ import { coin, dice, randomSeed } from 'pdrng';
233
+
234
+ // Built-in helper (uses crypto.getRandomValues for real entropy)
235
+ coin({ seed: randomSeed() }) // different each call
236
+
237
+ // Math.random() works directly as a seed
238
+ dice(6, { seed: Math.random() }) // different each call
239
+ ```
240
+
241
+ ## Imports
242
+
243
+ ```javascript
244
+ // Default import (with all methods attached)
245
+ import pdrng from 'pdrng';
246
+ pdrng.coin();
247
+
248
+ // Named imports
249
+ import { coin, dice, card, roll } from 'pdrng';
250
+ coin();
251
+ ```
252
+
253
+ ## Requirements
254
+
255
+ - Node.js 18+
256
+ - ESM only (`import`/`export`)
257
+
258
+ ## License
259
+
260
+ MIT
package/index.js ADDED
@@ -0,0 +1,716 @@
1
+ /**
2
+ * _
3
+ * _ __ __| |_ __ _ __ __ _
4
+ * | '_ \ / _` | '__| '_ \ / _` |
5
+ * | |_) | (_| | | | | | | (_| |
6
+ * | .__/ \__,_|_| |_| |_|\__, |
7
+ * |_| |___/
8
+ *
9
+ * pdrng - Pseudo Deterministic Random Number Generator
10
+ *
11
+ * A seed-based deterministic number generator.
12
+ * All outputs are fully reproducible given the same seed (default: 814).
13
+ *
14
+ * @module pdrng
15
+ * @version 1.0.0
16
+ * @license MIT
17
+ * @author Brian Funk
18
+ */
19
+
20
+ // ─── Default Seed ────────────────────────────────────────────────────────────
21
+
22
+ const DEFAULT_SEED = 814;
23
+
24
+ // ─── Private Helpers ─────────────────────────────────────────────────────────
25
+
26
+ /**
27
+ * Convert a text string to a numeric seed.
28
+ * Rolling XOR hash: each character mixes with the running hash via XOR shift.
29
+ * Formula: 2 * (rollingHash + length), where rollingHash starts at 3.
30
+ * Designed so that "brian" → 814.
31
+ * @param {string} text
32
+ * @returns {number}
33
+ */
34
+ const _textToSeed = (text) => {
35
+ const str = String(text);
36
+ let hash = 3;
37
+ for (let i = 0; i < str.length; i++) {
38
+ hash = hash + (str.charCodeAt(i) ^ (hash >> 2));
39
+ }
40
+ return 2 * (hash + str.length);
41
+ };
42
+
43
+ /**
44
+ * Normalize a seed value to a positive integer.
45
+ * Strings are converted via _textToSeed, numbers are floored and abs'd.
46
+ * @param {number|string} seed
47
+ * @returns {number}
48
+ */
49
+ const _normalizeSeed = (seed) => {
50
+ if (seed === undefined || seed === null) return DEFAULT_SEED;
51
+ if (typeof seed === 'string') return _textToSeed(seed);
52
+ const num = Number(seed);
53
+ if (!Number.isFinite(num)) return DEFAULT_SEED;
54
+ const abs = Math.abs(num);
55
+ // Handle floats between 0 and 1 (e.g. Math.random()) by scaling up
56
+ if (abs > 0 && abs < 1) {
57
+ const str = String(abs).replace('0.', '');
58
+ return Number(str) || DEFAULT_SEED;
59
+ }
60
+ const n = Math.floor(abs);
61
+ return n === 0 ? DEFAULT_SEED : n;
62
+ };
63
+
64
+ /**
65
+ * Get the array of digits from a seed.
66
+ * @param {number} seed
67
+ * @returns {number[]}
68
+ */
69
+ const _digits = (seed) => String(seed).split('').map(Number);
70
+
71
+ /**
72
+ * Sum of all digits.
73
+ * @param {number} seed
74
+ * @returns {number}
75
+ */
76
+ const _digitSum = (seed) => _digits(seed).reduce((a, b) => a + b, 0);
77
+
78
+ /**
79
+ * Product of all digits.
80
+ * @param {number} seed
81
+ * @returns {number}
82
+ */
83
+ const _digitProduct = (seed) => _digits(seed).reduce((a, b) => a * b, 1);
84
+
85
+ /**
86
+ * First digit of the seed.
87
+ * @param {number} seed
88
+ * @returns {number}
89
+ */
90
+ const _firstDigit = (seed) => _digits(seed)[0];
91
+
92
+ /**
93
+ * Last digit of the seed.
94
+ * @param {number} seed
95
+ * @returns {number}
96
+ */
97
+ const _lastDigit = (seed) => {
98
+ const d = _digits(seed);
99
+ return d[d.length - 1];
100
+ };
101
+
102
+ /**
103
+ * Last N digits of the seed as a number.
104
+ * @param {number} seed
105
+ * @param {number} n
106
+ * @returns {number}
107
+ */
108
+ const _lastN = (seed, n) => {
109
+ const str = String(seed);
110
+ if (n >= str.length) return seed;
111
+ return Number(str.slice(-n));
112
+ };
113
+
114
+ /**
115
+ * Digit-fill algorithm: produce a number with exactly `count` digits
116
+ * derived deterministically from the seed.
117
+ *
118
+ * Rules:
119
+ * - count < seed length: 1 digit → first digit, else last N digits
120
+ * - count = seed length: full seed
121
+ * - count > seed length: repeat full seed, remainder uses 1 char = first digit, 2+ chars = last N
122
+ *
123
+ * @param {number} seed
124
+ * @param {number} count
125
+ * @returns {number}
126
+ */
127
+ const _fillDigits = (seed, count) => {
128
+ const seedStr = String(seed);
129
+ const seedLen = seedStr.length;
130
+
131
+ if (count <= 0) return 0;
132
+
133
+ if (count < seedLen) {
134
+ if (count === 1) return _firstDigit(seed);
135
+ return _lastN(seed, count);
136
+ }
137
+
138
+ if (count === seedLen) return seed;
139
+
140
+ // count > seedLen: repeat seed, then fill remainder
141
+ let result = '';
142
+ const fullRepeats = Math.floor(count / seedLen);
143
+ const remainder = count % seedLen;
144
+
145
+ for (let i = 0; i < fullRepeats; i++) {
146
+ result += seedStr;
147
+ }
148
+
149
+ if (remainder > 0) {
150
+ if (remainder === 1) {
151
+ result += String(_firstDigit(seed));
152
+ } else {
153
+ result += String(_lastN(seed, remainder));
154
+ }
155
+ }
156
+
157
+ return Number(result);
158
+ };
159
+
160
+ /**
161
+ * Build a priority list of values derived from the seed.
162
+ * Order: full seed, last N-1 digits ... last 2 digits, first digit, last digit, middle digits L→R.
163
+ *
164
+ * @param {number} seed
165
+ * @returns {number[]}
166
+ */
167
+ const _seedPriority = (seed) => {
168
+ const seedStr = String(seed);
169
+ const n = seedStr.length;
170
+ const values = [seed];
171
+
172
+ // Last N-1 down to last 2 digits
173
+ for (let i = n - 1; i >= 2; i--) {
174
+ values.push(Number(seedStr.slice(-i)));
175
+ }
176
+
177
+ // First digit
178
+ values.push(Number(seedStr[0]));
179
+
180
+ // Last digit
181
+ if (n > 1) {
182
+ values.push(Number(seedStr[n - 1]));
183
+ }
184
+
185
+ // Remaining middle digits, left to right
186
+ for (let i = 1; i < n - 1; i++) {
187
+ values.push(Number(seedStr[i]));
188
+ }
189
+
190
+ return values;
191
+ };
192
+
193
+ /**
194
+ * Select a value from [min, max] using the seed priority algorithm.
195
+ * Returns the first priority value that falls within [min, max].
196
+ * Falls back to modulo-based selection if no priority value fits.
197
+ *
198
+ * @param {number} seed
199
+ * @param {number} min
200
+ * @param {number} max
201
+ * @returns {number}
202
+ */
203
+ const _selectFromRange = (seed, min, max) => {
204
+ const priorities = _seedPriority(seed);
205
+ for (const val of priorities) {
206
+ if (val >= min && val <= max) {
207
+ return val;
208
+ }
209
+ }
210
+ return min + (seed % (max - min + 1));
211
+ };
212
+
213
+ // ─── Data Constants ──────────────────────────────────────────────────────────
214
+
215
+ const MAGIC_8_RESPONSES = Object.freeze([
216
+ 'It is certain.',
217
+ 'It is decidedly so.',
218
+ 'Without a doubt.',
219
+ 'Yes — definitely.',
220
+ 'You may rely on it.',
221
+ 'As I see it, yes.',
222
+ 'Most likely.',
223
+ 'Outlook good.',
224
+ 'Yes.',
225
+ 'Signs point to yes.',
226
+ 'Reply hazy, try again.',
227
+ 'Ask again later.',
228
+ 'Better not tell you now.',
229
+ 'Cannot predict now.',
230
+ 'Reply hazy, try again.',
231
+ 'Don\'t count on it.',
232
+ 'My reply is no.',
233
+ 'My sources say no.',
234
+ 'Outlook not so good.',
235
+ 'Very doubtful.'
236
+ ]);
237
+
238
+ const MAJOR_ARCANA = Object.freeze([
239
+ 'The Magician',
240
+ 'The High Priestess',
241
+ 'The Empress',
242
+ 'The Emperor',
243
+ 'The Hierophant',
244
+ 'The Lovers',
245
+ 'The Chariot',
246
+ 'Strength',
247
+ 'The Hermit',
248
+ 'Wheel of Fortune',
249
+ 'Justice',
250
+ 'The Hanged Man',
251
+ 'Death',
252
+ 'Temperance',
253
+ 'The Devil',
254
+ 'The Tower',
255
+ 'The Star',
256
+ 'The Moon',
257
+ 'The Sun',
258
+ 'Judgement',
259
+ 'The World',
260
+ 'The Fool'
261
+ ]);
262
+
263
+ const FORTUNES = Object.freeze([
264
+ 'A beautiful, smart, and loving person will be coming into your life.',
265
+ 'A dubious friend may be an enemy in camouflage.',
266
+ 'A faithful friend is a strong defense.',
267
+ 'A feather in the hand is better than a bird in the air.',
268
+ 'A fresh start will put you on your way.',
269
+ 'A golden egg of opportunity falls into your lap this month.',
270
+ 'A good friendship is often more important than a passionate romance.',
271
+ 'A good time to finish up old tasks.',
272
+ 'A lifetime friend shall soon be made.',
273
+ 'A lifetime of happiness lies ahead of you.',
274
+ 'A light heart carries you through all the hard times.',
275
+ 'A new perspective will come with the new year.',
276
+ 'A pleasant surprise is waiting for you.',
277
+ 'The answer you seek was never in doubt.',
278
+ 'A smile is your passport into the hearts of others.',
279
+ 'A smooth long journey! Great expectations.',
280
+ 'A soft voice may be awfully persuasive.',
281
+ 'A true friend is the best possession.',
282
+ 'Accept something that you cannot change, and you will feel better.',
283
+ 'All the effort you are making will ultimately pay off.'
284
+ ]);
285
+
286
+ const SUITS = Object.freeze(['Spades', 'Diamonds', 'Hearts', 'Clubs']);
287
+
288
+ const RANKS = Object.freeze([
289
+ 'Ace', '2', '3', '4', '5', '6', '7', '8', '9', '10',
290
+ 'Jack', 'Queen', 'King'
291
+ ]);
292
+
293
+ const RPS_OPTIONS = Object.freeze(['rock', 'paper', 'scissors']);
294
+
295
+ const COIN_SIDES = Object.freeze(['heads', 'tails']);
296
+
297
+ const BINGO_LETTERS = Object.freeze(['B', 'I', 'N', 'G', 'O']);
298
+
299
+ const RED_NUMBERS = Object.freeze([
300
+ 1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36
301
+ ]);
302
+
303
+ const ZODIAC_SIGNS = Object.freeze([
304
+ { name: 'Aries', month: 3, startDay: 21 },
305
+ { name: 'Taurus', month: 4, startDay: 20 },
306
+ { name: 'Gemini', month: 5, startDay: 21 },
307
+ { name: 'Cancer', month: 6, startDay: 21 },
308
+ { name: 'Leo', month: 7, startDay: 23 },
309
+ { name: 'Virgo', month: 8, startDay: 23 },
310
+ { name: 'Libra', month: 9, startDay: 23 },
311
+ { name: 'Scorpio', month: 10, startDay: 23 },
312
+ { name: 'Sagittarius', month: 11, startDay: 22 },
313
+ { name: 'Capricorn', month: 12, startDay: 22 },
314
+ { name: 'Aquarius', month: 1, startDay: 20 },
315
+ { name: 'Pisces', month: 2, startDay: 19 }
316
+ ]);
317
+
318
+ // ─── Core Function ───────────────────────────────────────────────────────────
319
+
320
+ /**
321
+ * Generate a deterministic "random" number with the specified number of digits.
322
+ *
323
+ * @param {number} [digits=3] - Number of digits in the result
324
+ * @param {Object} [options={}] - Options
325
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
326
+ * @returns {number}
327
+ */
328
+ const pdrng = (digits = 3, options = {}) => {
329
+ const seed = _normalizeSeed(options.seed);
330
+ return _fillDigits(seed, digits);
331
+ };
332
+
333
+ // ─── Utility Functions ───────────────────────────────────────────────────────
334
+
335
+ /**
336
+ * Generate a deterministic float between 0 and 1.
337
+ *
338
+ * @param {number} [precision=6] - Number of decimal places
339
+ * @param {Object} [options={}] - Options
340
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
341
+ * @returns {number}
342
+ */
343
+ const float = (precision = 6, options = {}) => {
344
+ const seed = _normalizeSeed(options.seed);
345
+ const filled = _fillDigits(seed, precision);
346
+ return Number('0.' + String(filled).padStart(precision, '0'));
347
+ };
348
+
349
+ /**
350
+ * Generate a deterministic integer within a range (inclusive).
351
+ *
352
+ * @param {number} min - Minimum value
353
+ * @param {number} max - Maximum value
354
+ * @param {Object} [options={}] - Options
355
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
356
+ * @returns {number}
357
+ */
358
+ const range = (min, max, options = {}) => {
359
+ const seed = _normalizeSeed(options.seed);
360
+ return _selectFromRange(seed, min, max);
361
+ };
362
+
363
+ /**
364
+ * Generate an array of deterministic numbers.
365
+ * Each element uses a sub-seed derived from the main seed + index.
366
+ *
367
+ * @param {number} count - Number of elements
368
+ * @param {number} [digits=3] - Digits per element
369
+ * @param {Object} [options={}] - Options
370
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
371
+ * @returns {number[]}
372
+ */
373
+ const array = (count, digits = 3, options = {}) => {
374
+ const seed = _normalizeSeed(options.seed);
375
+ const result = [];
376
+ for (let i = 0; i < count; i++) {
377
+ const subSeed = seed + i * _digitProduct(seed);
378
+ result.push(_fillDigits(_normalizeSeed(subSeed), digits));
379
+ }
380
+ return result;
381
+ };
382
+
383
+ /**
384
+ * Generate a deterministic UUID (v4 format).
385
+ *
386
+ * @param {Object} [options={}] - Options
387
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
388
+ * @returns {string}
389
+ */
390
+ const uuid = (options = {}) => {
391
+ const seed = _normalizeSeed(options.seed);
392
+ const ds = _digitSum(seed);
393
+ const dp = _digitProduct(seed);
394
+ const fd = _firstDigit(seed);
395
+ const ld = _lastDigit(seed);
396
+
397
+ // Build 32 hex chars deterministically
398
+ const sources = [seed, ds, dp, fd, ld, seed + ds, seed + dp, seed * fd];
399
+ let hex = '';
400
+ for (const src of sources) {
401
+ let val = Math.abs(src);
402
+ for (let i = 0; i < 4; i++) {
403
+ hex += ((val + i * 7) % 16).toString(16);
404
+ }
405
+ }
406
+
407
+ // Format as UUID v4: xxxxxxxx-xxxx-4xxx-Nxxx-xxxxxxxxxxxx
408
+ hex = hex.slice(0, 32);
409
+ const chars = hex.split('');
410
+ chars[12] = '4'; // version 4
411
+ const n = (8 + (_digitSum(seed) % 4)); // variant: 8, 9, a, or b
412
+ chars[16] = n.toString(16);
413
+
414
+ return [
415
+ chars.slice(0, 8).join(''),
416
+ chars.slice(8, 12).join(''),
417
+ chars.slice(12, 16).join(''),
418
+ chars.slice(16, 20).join(''),
419
+ chars.slice(20, 32).join('')
420
+ ].join('-');
421
+ };
422
+
423
+ /**
424
+ * Determine if the seed is odd or even.
425
+ *
426
+ * @param {Object} [options={}] - Options
427
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
428
+ * @returns {string} "odd" or "even"
429
+ */
430
+ const oddOrEven = (options = {}) => {
431
+ const seed = _normalizeSeed(options.seed);
432
+ return seed % 2 === 0 ? 'even' : 'odd';
433
+ };
434
+
435
+ /**
436
+ * Determine red or black based on digit sum.
437
+ *
438
+ * @param {Object} [options={}] - Options
439
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
440
+ * @returns {string} "red" or "black"
441
+ */
442
+ const redOrBlack = (options = {}) => {
443
+ const seed = _normalizeSeed(options.seed);
444
+ return _digitSum(seed) % 2 === 1 ? 'red' : 'black';
445
+ };
446
+
447
+ /**
448
+ * Generate a random seed using crypto for true entropy.
449
+ * Pass the result as { seed: randomSeed() } to any pdrng function
450
+ * for non-deterministic behavior.
451
+ *
452
+ * @returns {number}
453
+ */
454
+ const randomSeed = () => {
455
+ if (typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues) {
456
+ const buffer = new Uint32Array(1);
457
+ globalThis.crypto.getRandomValues(buffer);
458
+ return buffer[0] || DEFAULT_SEED;
459
+ }
460
+ // Fallback for environments without Web Crypto API
461
+ return Math.floor(Math.random() * 2147483647) || DEFAULT_SEED;
462
+ };
463
+
464
+ // ─── Simulation Functions ────────────────────────────────────────────────────
465
+
466
+ /**
467
+ * Flip a deterministic coin.
468
+ *
469
+ * @param {Object} [options={}] - Options
470
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
471
+ * @returns {string} "heads" or "tails"
472
+ */
473
+ const coin = (options = {}) => {
474
+ const seed = _normalizeSeed(options.seed);
475
+ return COIN_SIDES[_digitSum(seed) % 2];
476
+ };
477
+
478
+ /**
479
+ * Roll a deterministic die.
480
+ *
481
+ * @param {number} [sides=6] - Number of sides
482
+ * @param {Object} [options={}] - Options
483
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
484
+ * @returns {number} 1 to sides
485
+ */
486
+ const dice = (sides = 6, options = {}) => {
487
+ const seed = _normalizeSeed(options.seed);
488
+ return _selectFromRange(seed, 1, sides);
489
+ };
490
+
491
+ /**
492
+ * Draw a deterministic playing card.
493
+ *
494
+ * @param {Object} [options={}] - Options
495
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
496
+ * @returns {string} e.g. "8 of Diamonds"
497
+ */
498
+ const card = (options = {}) => {
499
+ const seed = _normalizeSeed(options.seed);
500
+ const rankIndex = (seed - 1) % 13;
501
+ const suitIndex = _digitSum(seed) % 4;
502
+ return `${RANKS[rankIndex]} of ${SUITS[suitIndex]}`;
503
+ };
504
+
505
+ /**
506
+ * Spin the roulette wheel deterministically.
507
+ *
508
+ * @param {Object} [options={}] - Options
509
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
510
+ * @returns {Object} { number, color, parity }
511
+ */
512
+ const roulette = (options = {}) => {
513
+ const seed = _normalizeSeed(options.seed);
514
+ const num = _lastN(seed, 2) % 37;
515
+
516
+ let color;
517
+ if (num === 0) {
518
+ color = 'green';
519
+ } else if (RED_NUMBERS.includes(num)) {
520
+ color = 'red';
521
+ } else {
522
+ color = 'black';
523
+ }
524
+
525
+ const parity = num === 0 ? 'zero' : (num % 2 === 0 ? 'even' : 'odd');
526
+
527
+ return { number: num, color, parity };
528
+ };
529
+
530
+ /**
531
+ * Play rock, paper, scissors deterministically.
532
+ *
533
+ * @param {Object} [options={}] - Options
534
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
535
+ * @returns {string} "rock", "paper", or "scissors"
536
+ */
537
+ const rps = (options = {}) => {
538
+ const seed = _normalizeSeed(options.seed);
539
+ return RPS_OPTIONS[_digitProduct(seed) % 3];
540
+ };
541
+
542
+ /**
543
+ * Shake the Magic 8-Ball deterministically.
544
+ *
545
+ * @param {Object} [options={}] - Options
546
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
547
+ * @returns {string}
548
+ */
549
+ const magic8 = (options = {}) => {
550
+ const seed = _normalizeSeed(options.seed);
551
+ return MAGIC_8_RESPONSES[seed % 20];
552
+ };
553
+
554
+ /**
555
+ * Determine your deterministic zodiac sign.
556
+ *
557
+ * @param {Object} [options={}] - Options
558
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
559
+ * @returns {string} e.g. "Gemini"
560
+ */
561
+ const zodiac = (options = {}) => {
562
+ const seed = _normalizeSeed(options.seed);
563
+ const signIndex = _lastN(seed, 2) % 12;
564
+ return ZODIAC_SIGNS[signIndex].name;
565
+ };
566
+
567
+ /**
568
+ * Draw a deterministic tarot card (Major Arcana).
569
+ *
570
+ * @param {Object} [options={}] - Options
571
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
572
+ * @returns {string}
573
+ */
574
+ const tarot = (options = {}) => {
575
+ const seed = _normalizeSeed(options.seed);
576
+ return MAJOR_ARCANA[seed % 22];
577
+ };
578
+
579
+ /**
580
+ * Receive a deterministic fortune.
581
+ *
582
+ * @param {Object} [options={}] - Options
583
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
584
+ * @returns {string}
585
+ */
586
+ const fortune = (options = {}) => {
587
+ const seed = _normalizeSeed(options.seed);
588
+ return FORTUNES[_digitSum(seed) % 20];
589
+ };
590
+
591
+ /**
592
+ * Spin a wheel (pick from an array) deterministically.
593
+ *
594
+ * @param {Array} arr - Array of choices
595
+ * @param {Object} [options={}] - Options
596
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
597
+ * @returns {*}
598
+ */
599
+ const spin = (arr, options = {}) => {
600
+ if (!Array.isArray(arr) || arr.length === 0) {
601
+ throw new Error('spin() requires a non-empty array');
602
+ }
603
+ const seed = _normalizeSeed(options.seed);
604
+ return arr[seed % arr.length];
605
+ };
606
+
607
+ /**
608
+ * Roll dice using standard notation (e.g. "2d6+3").
609
+ *
610
+ * @param {string} notation - Dice notation like "2d6", "1d20+5", "3d8-2"
611
+ * @param {Object} [options={}] - Options
612
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
613
+ * @returns {Object} { rolls, modifier, total }
614
+ */
615
+ const roll = (notation, options = {}) => {
616
+ const seed = _normalizeSeed(options.seed);
617
+ const match = String(notation).match(/^(\d+)d(\d+)([+-]\d+)?$/);
618
+ if (!match) {
619
+ throw new Error(`Invalid dice notation: "${notation}"`);
620
+ }
621
+
622
+ const count = parseInt(match[1], 10);
623
+ const sides = parseInt(match[2], 10);
624
+ const modifier = match[3] ? parseInt(match[3], 10) : 0;
625
+ const dp = _digitProduct(seed);
626
+
627
+ const rolls = [];
628
+ for (let i = 0; i < count; i++) {
629
+ const val = ((seed + i * dp) % sides) + 1;
630
+ rolls.push(val);
631
+ }
632
+
633
+ const total = rolls.reduce((a, b) => a + b, 0) + modifier;
634
+ return { rolls, modifier, total };
635
+ };
636
+
637
+ /**
638
+ * Call a bingo number deterministically.
639
+ *
640
+ * @param {Object} [options={}] - Options
641
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
642
+ * @returns {string} e.g. "B-14"
643
+ */
644
+ const bingo = (options = {}) => {
645
+ const seed = _normalizeSeed(options.seed);
646
+ const num = ((_lastN(seed, 2) - 1) % 75 + 75) % 75 + 1;
647
+ const letterIndex = Math.floor((num - 1) / 15);
648
+ return `${BINGO_LETTERS[letterIndex]}-${num}`;
649
+ };
650
+
651
+ /**
652
+ * Generate a deterministic hex color.
653
+ *
654
+ * @param {Object} [options={}] - Options
655
+ * @param {number|string} [options.seed] - Custom seed (default: 814)
656
+ * @returns {string} e.g. "#a81414"
657
+ */
658
+ const color = (options = {}) => {
659
+ const seed = _normalizeSeed(options.seed);
660
+ const prefix = (_firstDigit(seed) + 2).toString(16);
661
+ const fill = String(_fillDigits(seed, 5));
662
+ return '#' + prefix + fill;
663
+ };
664
+
665
+ // ─── Attach Methods ──────────────────────────────────────────────────────────
666
+
667
+ pdrng.float = float;
668
+ pdrng.range = range;
669
+ pdrng.array = array;
670
+ pdrng.uuid = uuid;
671
+ pdrng.oddOrEven = oddOrEven;
672
+ pdrng.redOrBlack = redOrBlack;
673
+ pdrng.coin = coin;
674
+ pdrng.dice = dice;
675
+ pdrng.card = card;
676
+ pdrng.roulette = roulette;
677
+ pdrng.rps = rps;
678
+ pdrng.magic8 = magic8;
679
+ pdrng.zodiac = zodiac;
680
+ pdrng.tarot = tarot;
681
+ pdrng.fortune = fortune;
682
+ pdrng.spin = spin;
683
+ pdrng.roll = roll;
684
+ pdrng.bingo = bingo;
685
+ pdrng.color = color;
686
+ pdrng.randomSeed = randomSeed;
687
+ pdrng.DEFAULT_SEED = DEFAULT_SEED;
688
+
689
+ // ─── Exports ─────────────────────────────────────────────────────────────────
690
+
691
+ export default pdrng;
692
+
693
+ export {
694
+ pdrng,
695
+ float,
696
+ range,
697
+ array,
698
+ uuid,
699
+ oddOrEven,
700
+ redOrBlack,
701
+ coin,
702
+ dice,
703
+ card,
704
+ roulette,
705
+ rps,
706
+ magic8,
707
+ zodiac,
708
+ tarot,
709
+ fortune,
710
+ spin,
711
+ roll,
712
+ bingo,
713
+ color,
714
+ randomSeed,
715
+ DEFAULT_SEED
716
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "pdrng",
3
+ "version": "1.0.0",
4
+ "description": "Pseudo Deterministic Random Number Generator - seed-based deterministic number generation",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./index.js"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage",
16
+ "lint": "eslint . --format stylish",
17
+ "lint:report": "eslint . --format html -o coverage/lint-report.html"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/brianfunk/pdrng.git"
22
+ },
23
+ "keywords": [
24
+ "random",
25
+ "rng",
26
+ "prng",
27
+ "deterministic",
28
+ "seed",
29
+ "dice",
30
+ "coin",
31
+ "uuid",
32
+ "pseudo",
33
+ "number",
34
+ "generator"
35
+ ],
36
+ "author": "Brian Funk",
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/brianfunk/pdrng/issues"
40
+ },
41
+ "homepage": "https://github.com/brianfunk/pdrng#readme",
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "devDependencies": {
46
+ "@vitest/coverage-v8": "^4.0.18",
47
+ "eslint": "^9.17.0",
48
+ "vitest": "^4.0.18"
49
+ }
50
+ }