playlist-data-engine 1.7.3 → 1.8.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.
Files changed (35) hide show
  1. package/README.md +14 -0
  2. package/bin/cli.cjs +85 -0
  3. package/dist/gateway-CDMPqFEH.js +1320 -0
  4. package/dist/gateway-DKa45Uz6.cjs +6 -0
  5. package/dist/gateway.d.ts +1 -0
  6. package/dist/gateway.d.ts.map +1 -1
  7. package/dist/gateway.js +1 -1
  8. package/dist/gateway.mjs +22 -19
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/playlist-data-engine.js +4 -4
  12. package/dist/playlist-data-engine.mjs +30 -27
  13. package/dist/utils/engineDocs.d.ts +33 -0
  14. package/dist/utils/engineDocs.d.ts.map +1 -0
  15. package/docs/DATA_ENGINE_REFERENCE.md +6660 -0
  16. package/docs/USAGE_IN_OTHER_PROJECTS.md +587 -0
  17. package/docs/features/AUDIO_ANALYSIS.md +610 -0
  18. package/docs/features/BEAT_DETECTION.md +5250 -0
  19. package/docs/features/COMBAT_SYSTEM.md +1632 -0
  20. package/docs/features/CONTENT_PACKS.md +464 -0
  21. package/docs/features/CUSTOM_CONTENT.md +603 -0
  22. package/docs/features/ENEMY_GENERATION.md +1711 -0
  23. package/docs/features/EQUIPMENT_SYSTEM.md +2279 -0
  24. package/docs/features/EXTENSIBILITY_GUIDE.md +1106 -0
  25. package/docs/features/GATEWAY_RESOLUTION.md +725 -0
  26. package/docs/features/IRL_SENSORS.md +360 -0
  27. package/docs/features/PLAYLIST_PARSING.md +446 -0
  28. package/docs/features/PREREQUISITES.md +571 -0
  29. package/docs/features/ROLLS_AND_SEEDS.md +687 -0
  30. package/docs/features/XP_AND_STATS.md +1221 -0
  31. package/llms.txt +33 -0
  32. package/package.json +9 -2
  33. package/skills/playlist-data-engine/SKILL.md +69 -0
  34. package/dist/gateway-C_p9Ku3O.js +0 -1211
  35. package/dist/gateway-Ceg-5xug.cjs +0 -1
@@ -0,0 +1,687 @@
1
+ # Randomness Reference
2
+
3
+ Complete guide to the dice roller and seeded randomness in the Playlist Data Engine.
4
+
5
+ **For API details, see [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md)**
6
+ **For other usage examples, see [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md)**
7
+
8
+ ---
9
+
10
+ ## Table of Contents
11
+
12
+ 1. [Hash Utilities and Deterministic Seeding](#hash-utilities-and-deterministic-seeding)
13
+ 2. [Dice Roller](#dice-roller)
14
+ 3. [Seeded Dice Roller](#seeded-dice-roller)
15
+ 4. [Initiative Roller](#initiative-roller)
16
+ 5. [Seeded RNG in Combat Simulations](#seeded-rng-in-combat-simulations)
17
+
18
+ ---
19
+
20
+
21
+ ## Hash Utilities and Deterministic Seeding
22
+
23
+ The hash utilities provide deterministic seed generation for reproducible character generation:
24
+
25
+ ```typescript
26
+ import { generateSeed, hashSeedToFloat, hashSeedToInt, deriveSeed, SeededRNG } from 'playlist-data-engine';
27
+
28
+ // Generate a deterministic seed from blockchain data
29
+ // Takes THREE parameters: chainName, tokenAddress, tokenId
30
+ const seed = generateSeed('ethereum', '0x123abc...', '42');
31
+ console.log(seed); // "ethereum-0x123abc...-42"
32
+
33
+ // Hash seed to float (0.0 - 1.0)
34
+ const float = hashSeedToFloat(seed);
35
+ console.log(float); // e.g., 0.6423...
36
+
37
+ // Hash seed to integer in range
38
+ const stat = hashSeedToInt(seed, 8, 18); // Random stat between 8 and 17
39
+ console.log(stat); // e.g., 14
40
+
41
+ // Derive new seeds for related random values
42
+ const raceSeed = deriveSeed(seed, 'race');
43
+ const classSeed = deriveSeed(seed, 'class');
44
+ const statsSeed = deriveSeed(seed, 'stats');
45
+ console.log(raceSeed); // "ethereum-0x123abc...-42:race"
46
+
47
+ // Use SeededRNG for complex deterministic random operations
48
+ const rng = new SeededRNG(seed);
49
+
50
+ // Generate random float in [0.0, 1.0)
51
+ const randomValue = rng.random();
52
+
53
+ // Generate random integer in range [min, max)
54
+ const d20Roll = rng.randomInt(1, 21);
55
+ const damage = rng.randomInt(1, 9); // 1d8
56
+
57
+ // Pick random element from array
58
+ const races = ['Human', 'Elf', 'Dwarf', 'Halfling'];
59
+ const race = rng.randomChoice(races);
60
+
61
+ // Pick weighted random element (uses [value, weight] tuples)
62
+ const treasureOptions = [
63
+ ['Gold', 50],
64
+ ['Gem', 30],
65
+ ['Artifact', 10]
66
+ ];
67
+ const item = rng.weightedChoice(treasureOptions);
68
+ console.log(item); // 'Gold' (50% chance), 'Gem' (30% chance), or 'Artifact' (10% chance)
69
+
70
+ // Shuffle array deterministically
71
+ const cards = ['A', 'K', 'Q', 'J', '10', '9', '8', '7'];
72
+ const shuffled = rng.shuffle([...cards]);
73
+ ```
74
+
75
+ **Common Use Case: Blockchain-Based Character Generation**
76
+
77
+ ```typescript
78
+ import { generateSeed, CharacterGenerator } from 'playlist-data-engine';
79
+ import { AudioAnalyzer } from 'playlist-data-engine/analysis';
80
+
81
+ // Given an NFT's blockchain data
82
+ const nftData = {
83
+ chain: 'ethereum',
84
+ contractAddress: '0x1234567890abcdef...',
85
+ tokenId: '1234'
86
+ };
87
+
88
+ // Generate a deterministic seed
89
+ const seed = generateSeed(nftData.chain, nftData.contractAddress, nftData.tokenId);
90
+
91
+ // Generate character from seed and audio
92
+ const analyzer = new AudioAnalyzer();
93
+ const audio = await analyzer.extractSonicFingerprint(track.audio_url);
94
+ const character = CharacterGenerator.generate(seed, audio, track);
95
+
96
+ // The same NFT always generates the same character!
97
+ ```
98
+
99
+ ---
100
+
101
+
102
+ ## Dice Roller
103
+
104
+ The `DiceRoller` class provides utility methods for D&D-style dice rolling. All methods are static—call them directly without instantiation.
105
+
106
+ ```typescript
107
+ import { DiceRoller } from 'playlist-data-engine';
108
+
109
+ // Basic dice rolling
110
+ const d6Result = DiceRoller.rollDie(6); // Roll a single d6 (1-6)
111
+ const d20Result = DiceRoller.rollD20(); // Roll a d20 (1-20)
112
+ const threeD6 = DiceRoller.rollMultipleDice(3, 6); // Roll 3d6, returns [3, 5, 2]
113
+ const percentile = DiceRoller.rollPercentile(); // Roll d100 (1-100)
114
+
115
+ // Parse and roll dice formulas
116
+ const fireball = DiceRoller.parseDiceFormula('8d6+5');
117
+ console.log(`Fireball damage: ${fireball.total}`); // Sum of all rolls + modifier
118
+ console.log(`Individual rolls: ${fireball.rolls}`); // Array of each die result
119
+
120
+ // Advantage and disadvantage
121
+ const advRoll = DiceRoller.rollWithAdvantage();
122
+ console.log(`Rolled ${advRoll.roll1} and ${advRoll.roll2}, taking ${advRoll.result}`);
123
+
124
+ const disadvRoll = DiceRoller.rollWithDisadvantage();
125
+ console.log(`Rolled ${disadvRoll.roll1} and ${disadvRoll.roll2}, taking ${disadvRoll.result}`);
126
+
127
+ // Combat functions
128
+ const initiative = DiceRoller.rollInitiative(3); // d20 + DEX modifier (e.g., +3)
129
+
130
+ const damage = DiceRoller.calculateDamage('2d6', 2, false); // formula, modifier, critical?
131
+ console.log(`Damage: ${damage.total} (${damage.rolls} + ${damage.modifier})`);
132
+
133
+ const critDamage = DiceRoller.calculateDamage('2d6', 2, true); // Critical hit - dice doubled
134
+ console.log(`Critical damage: ${critDamage.total}`);
135
+
136
+ // Manual critical handling
137
+ const baseRolls = DiceRoller.rollMultipleDice(2, 6); // [4, 3]
138
+ const critRolls = DiceRoller.doubleDamage(baseRolls); // [4, 3, 4, 3]
139
+
140
+ // Saving throws and ability checks
141
+ const fortitudeSave = DiceRoller.rollSavingThrow(2, 2); // ability modifier + proficiency bonus
142
+ const athleticsCheck = DiceRoller.rollAbilityCheck(4, 0); // ability modifier only
143
+
144
+ // Critical detection
145
+ const attackRoll = DiceRoller.rollD20();
146
+ if (DiceRoller.isCriticalHit(attackRoll)) {
147
+ console.log('Critical hit! Double the damage dice!');
148
+ }
149
+ if (DiceRoller.isCriticalMiss(attackRoll)) {
150
+ console.log('Critical miss! Attack fails automatically.');
151
+ }
152
+
153
+ // Seeded RNG for reproducible rolls
154
+ const seeded = DiceRoller.seededRoll(12345); // Same seed always produces same result
155
+ const anotherSeeded = DiceRoller.seededRoll(12345); // Will equal seeded
156
+ ```
157
+
158
+ **Common Use Case: Custom Attack Resolution**
159
+
160
+ ```typescript
161
+ import { DiceRoller } from 'playlist-data-engine';
162
+
163
+ function resolveAttack(attackBonus: number, targetAC: number, hasAdvantage: boolean, hasDisadvantage: boolean) {
164
+ let d20Roll: number;
165
+ let roll1: number | undefined;
166
+ let roll2: number | undefined;
167
+ let isCritical: boolean;
168
+ let isMiss: boolean;
169
+
170
+ if (hasAdvantage) {
171
+ const result = DiceRoller.rollWithAdvantage();
172
+ roll1 = result.roll1;
173
+ roll2 = result.roll2;
174
+ d20Roll = result.result;
175
+ // D&D 5e Sage Advice: With advantage, if EITHER die is a 20, it's a critical hit
176
+ isCritical = DiceRoller.isCriticalHit(roll1) || DiceRoller.isCriticalHit(roll2);
177
+ // With advantage, only check for fumble on the selected roll
178
+ isMiss = DiceRoller.isCriticalMiss(d20Roll);
179
+ console.log(`Advantage: rolled ${roll1} and ${roll2}, using ${d20Roll}`);
180
+ } else if (hasDisadvantage) {
181
+ const result = DiceRoller.rollWithDisadvantage();
182
+ roll1 = result.roll1;
183
+ roll2 = result.roll2;
184
+ d20Roll = result.result;
185
+ // D&D 5e Sage Advice: With disadvantage, if EITHER die is a 1, it's a critical miss
186
+ isMiss = DiceRoller.isCriticalMiss(roll1) || DiceRoller.isCriticalMiss(roll2);
187
+ // With disadvantage, only check for crit on the selected roll
188
+ isCritical = DiceRoller.isCriticalHit(d20Roll);
189
+ console.log(`Disadvantage: rolled ${roll1} and ${roll2}, using ${d20Roll}`);
190
+ } else {
191
+ d20Roll = DiceRoller.rollD20();
192
+ isCritical = DiceRoller.isCriticalHit(d20Roll);
193
+ isMiss = DiceRoller.isCriticalMiss(d20Roll);
194
+ }
195
+
196
+ const total = d20Roll + attackBonus;
197
+ const hit = !isMiss && (isCritical || total >= targetAC);
198
+
199
+ return { d20Roll, roll1, roll2, total, hit, isCritical, isMiss };
200
+ }
201
+
202
+ const attack = resolveAttack(7, 15, true, false);
203
+ console.log(`Attack roll: ${attack.d20Roll} + 7 = ${attack.total} vs AC 15`);
204
+ if (attack.isCritical) {
205
+ console.log('CRITICAL HIT!');
206
+ } else if (attack.isMiss) {
207
+ console.log('CRITICAL MISS!');
208
+ } else if (attack.hit) {
209
+ console.log('Hit!');
210
+ } else {
211
+ console.log('Miss!');
212
+ }
213
+ ```
214
+
215
+ **Important:** D&D 5e rules for advantage/disadvantage with critical hits and misses:
216
+ - **Advantage:** If EITHER die shows a 20, it's a critical hit
217
+ - **Disadvantage:** If EITHER die shows a 1, it's a critical miss
218
+ - This follows the official D&D 5e Sage Advice ruling
219
+
220
+ ---
221
+
222
+ ## Seeded Dice Roller
223
+
224
+ The `SeededDiceRoller` class provides the same API as the static `DiceRoller` but produces **deterministic** results. It wraps `SeededRNG` internally, so the same seed and call sequence always produces identical rolls.
225
+
226
+ ### Key Differences from DiceRoller
227
+
228
+ | Feature | `DiceRoller` (static) | `SeededDiceRoller` (instance) |
229
+ |---------|----------------------|------------------------------|
230
+ | Usage | Static methods: `DiceRoller.rollD20()` | Instance methods: `roller.rollD20()` |
231
+ | Randomness | `Math.random()` — non-deterministic | `SeededRNG` — deterministic |
232
+ | State | No state (each call independent) | Stateful counter (call order matters) |
233
+ | Use case | Live gameplay, one-off rolls | Simulations, reproducible tests |
234
+
235
+ ### Creating a Seeded Roller
236
+
237
+ Three ways to create an instance:
238
+
239
+ ```typescript
240
+ import { SeededDiceRoller, createSeededRoller, SeededRNG } from 'playlist-data-engine';
241
+
242
+ // 1. Factory function (recommended)
243
+ const roller = createSeededRoller('my-simulation-seed');
244
+
245
+ // 2. Constructor with seed string
246
+ const roller2 = new SeededDiceRoller('another-seed');
247
+
248
+ // 3. Constructor with existing SeededRNG instance
249
+ const rng = new SeededRNG('pre-configured-rng');
250
+ const roller3 = new SeededDiceRoller(rng);
251
+ ```
252
+
253
+ The factory function `createSeededRoller()` is the standard way to create rollers — it's what `CombatSimulator` and `AICombatRunner` use internally.
254
+
255
+ ### API Reference
256
+
257
+ All methods mirror the static `DiceRoller` API. The key difference is that results are deterministic given the same seed and call order.
258
+
259
+ | Method | Returns | Description |
260
+ |--------|---------|-------------|
261
+ | `rollDie(sides)` | `number` | Roll a single die (1 to sides) |
262
+ | `rollD20()` | `number` | Roll a d20 (1-20) |
263
+ | `rollMultipleDice(count, sides)` | `number[]` | Roll N dice, return individual results |
264
+ | `parseDiceFormula(formula)` | `{ diceCount, diceSides, modifier, rolls, total }` | Parse and roll formulas like `"2d6+3"` |
265
+ | `rollWithAdvantage()` | `{ roll1, roll2, result }` | Roll 2d20, take higher |
266
+ | `rollWithDisadvantage()` | `{ roll1, roll2, result }` | Roll 2d20, take lower |
267
+ | `rollInitiative(dexModifier)` | `number` | d20 + DEX modifier |
268
+ | `calculateDamage(formula, modifier, isCritical?)` | `{ diceFormula, rolls, modifier, total, isCritical }` | Roll damage with optional crit doubling |
269
+ | `rollSavingThrow(abilityMod, proficiency?)` | `number` | d20 + ability mod + proficiency |
270
+ | `rollAbilityCheck(abilityMod, proficiency?)` | `number` | d20 + ability mod + proficiency |
271
+ | `rollPercentile()` | `number` | Roll d100 (1-100) |
272
+ | `isCriticalHit(d20Roll)` | `boolean` | Check if roll is natural 20 |
273
+ | `isCriticalMiss(d20Roll)` | `boolean` | Check if roll is natural 1 |
274
+ | `doubleDamage(rolls)` | `number[]` | Double the dice array (for crits) |
275
+
276
+ ### Usage Examples
277
+
278
+ **Basic deterministic rolling:**
279
+
280
+ ```typescript
281
+ import { createSeededRoller } from 'playlist-data-engine';
282
+
283
+ const roller = createSeededRoller('combat-42');
284
+
285
+ // These are always the same for seed 'combat-42'
286
+ console.log(roller.rollD20()); // e.g., 14
287
+ console.log(roller.rollD20()); // e.g., 7 (next in sequence)
288
+ console.log(roller.rollDie(8)); // e.g., 3
289
+
290
+ // A different roller with the same seed starts from the beginning
291
+ const roller2 = createSeededRoller('combat-42');
292
+ console.log(roller2.rollD20()); // 14 (same as first call above)
293
+ ```
294
+
295
+ **Full combat simulation with seeded dice:**
296
+
297
+ ```typescript
298
+ import { createSeededRoller } from 'playlist-data-engine';
299
+
300
+ // Create a roller for this simulation run
301
+ const roller = createSeededRoller('balance-test-1-0');
302
+
303
+ // Use it for all combat rolls — every roll is deterministic
304
+ const attackRoll = roller.rollD20(); // Attack: 14
305
+ const damage = roller.calculateDamage('2d6', 3); // Damage: { total: 11, rolls: [4, 4] }
306
+ const savingThrow = roller.rollSavingThrow(-1, 2); // Save: 10
307
+ const initiative = roller.rollInitiative(2); // Initiative: 16
308
+ ```
309
+
310
+ ### Injecting into CombatEngine
311
+
312
+ Pass a `SeededDiceRoller` to `CombatEngine` to make all combat rolls deterministic:
313
+
314
+ ```typescript
315
+ import { CombatEngine, createSeededRoller } from 'playlist-data-engine';
316
+
317
+ // Non-deterministic (live gameplay)
318
+ const liveEngine = new CombatEngine({ maxTurnsBeforeDraw: 50 });
319
+ // Uses Math.random() internally
320
+
321
+ // Deterministic (simulation)
322
+ const simEngine = new CombatEngine({}, createSeededRoller('sim-seed'));
323
+ // All attack rolls, damage, saving throws, and initiative are deterministic
324
+ ```
325
+
326
+ The injected roller flows to all combat subsystems:
327
+
328
+ ```
329
+ CombatEngine (receives roller)
330
+ ├── AttackResolver → attack rolls, damage rolls, hit/miss
331
+ ├── InitiativeRoller → initiative ordering
332
+ └── SpellCaster → spell attack rolls, saving throws
333
+ ```
334
+
335
+ ### How CombatSimulator Manages Seeding
336
+
337
+ The `CombatSimulator` creates a fresh `SeededDiceRoller` for each simulation run, ensuring no state leaks between runs. Each run gets a unique seed derived from the base seed:
338
+
339
+ ```typescript
340
+ // Internal logic (simplified):
341
+ for (let i = 0; i < config.runCount; i++) {
342
+ const runSeed = `${config.baseSeed}-${i}`;
343
+ const roller = createSeededRoller(runSeed);
344
+ const runner = new AICombatRunner();
345
+ const result = runner.runFullCombat(players, enemies, aiConfig, combatConfig, roller);
346
+ aggregator.aggregateRun(result, i, runSeed);
347
+ }
348
+ ```
349
+
350
+ This means:
351
+ - **Run 0** gets seed `"my-base-seed-0"` — completely independent RNG state
352
+ - **Run 1** gets seed `"my-base-seed-1"` — fresh roller, no carry-over from Run 0
353
+ - Changing `baseSeed` changes all runs; changing `runCount` adds/removes runs without affecting existing ones
354
+
355
+ > **Note:** The enemy generation seed (used by `EnemyGenerator`) is separate from the simulation seed. Changing the simulation seed does not change which enemies are generated — only how the combat dice fall. See [Seeded RNG in Combat Simulations](#seeded-rng-in-combat-simulations) for the full seeding architecture.
356
+
357
+ ---
358
+
359
+ ## Initiative Roller
360
+
361
+ The `InitiativeRoller` class manages the D&D 5e initiative system for combat. It handles rolling initiative, sorting combatants by turn order, and managing turn progression.
362
+
363
+ ```typescript
364
+ import { InitiativeRoller } from 'playlist-data-engine';
365
+
366
+ const roller = new InitiativeRoller();
367
+ ```
368
+
369
+ ### Rolling Initiative
370
+
371
+ Roll initiative for a single combatant:
372
+
373
+ ```typescript
374
+ const combatant = {
375
+ id: 'hero-1',
376
+ character: {
377
+ name: 'Aragorn',
378
+ ability_modifiers: { dexterity: 3 }
379
+ },
380
+ initiative: 0
381
+ };
382
+
383
+ const result = roller.rollInitiativeForCombatant(combatant);
384
+ console.log(`${result.combatant.character.name} rolls ${result.d20Roll} + ${result.dexModifier} = ${result.initiativeTotal}`);
385
+ // Output: "Aragorn rolls 15 + 3 = 18"
386
+ ```
387
+
388
+ Roll initiative for all combatants at once:
389
+
390
+ ```typescript
391
+ const combatants = [
392
+ { id: 'hero-1', character: { name: 'Aragorn', ability_modifiers: { dexterity: 3 } }, initiative: 0 },
393
+ { id: 'hero-2', character: { name: 'Gimli', ability_modifiers: { dexterity: -1 } }, initiative: 0 },
394
+ { id: 'enemy-1', character: { name: 'Orc', ability_modifiers: { dexterity: 0 } }, initiative: 0 }
395
+ ];
396
+
397
+ const { results, sortedCombatants } = roller.rollInitiativeForAll(combatants);
398
+
399
+ console.log('Initiative Results:');
400
+ results.forEach(r => {
401
+ console.log(` ${r.combatant.character.name}: ${r.d20Roll} + ${r.dexModifier} = ${r.initiativeTotal}`);
402
+ });
403
+
404
+ console.log('\nTurn Order:');
405
+ sortedCombatants.forEach((c, i) => {
406
+ console.log(` ${i + 1}. ${c.character.name} (Initiative: ${c.initiative})`);
407
+ });
408
+ ```
409
+
410
+ ### Turn Management
411
+
412
+ Get the next combatant in turn order:
413
+
414
+ ```typescript
415
+ let currentIndex = 0;
416
+ const combatants = sortedCombatants; // from rollInitiativeForAll
417
+
418
+ const { combatant, index, isNewRound } = roller.getNextCombatant(combatants, currentIndex);
419
+ console.log(`Next up: ${combatant.character.name}${isNewRound ? ' (NEW ROUND!)' : ''}`);
420
+ currentIndex = index;
421
+ ```
422
+
423
+ Get formatted initiative order for display:
424
+
425
+ ```typescript
426
+ const order = roller.getInitiativeOrder(combatants);
427
+ order.forEach(line => console.log(line));
428
+ // Output:
429
+ // 1. Aragorn (Initiative: 18, DEX: 3)
430
+ // 2. Orc (Initiative: 12, DEX: 0)
431
+ // 3. Gimli (Initiative: 8, DEX: -1)
432
+ ```
433
+
434
+ ### Mid-Combat Changes
435
+
436
+ Re-roll initiative for a specific combatant (e.g., after a DEX-changing effect):
437
+
438
+ ```typescript
439
+ const newInitiative = roller.rerollInitiativeForCombatant(combatant);
440
+ console.log(`${combatant.character.name} rerolls initiative: ${newInitiative}`);
441
+ ```
442
+
443
+ Delay a combatant's turn (used with the "Ready" action):
444
+
445
+ ```typescript
446
+ const delayedOrder = roller.delayTurn(combatants, 'hero-1');
447
+ // hero-1 moves to the next position in the initiative order
448
+ ```
449
+
450
+ Re-sort combatants by initiative (e.g., when new combatants join mid-fight):
451
+
452
+ ```typescript
453
+ combatants.push(newCombatant);
454
+ const resortOrder = roller.resortByInitiative(combatants);
455
+ // All combatants re-sorted by their current initiative values
456
+ ```
457
+
458
+ ### Full Combat Workflow Example
459
+
460
+ ```typescript
461
+ import { InitiativeRoller, DiceRoller } from 'playlist-data-engine';
462
+
463
+ // Setup combat
464
+ const roller = new InitiativeRoller();
465
+ const combatants = [
466
+ { id: 'fighter', character: { name: 'Fighter', ability_modifiers: { dexterity: 2 } }, initiative: 0 },
467
+ { id: 'goblin', character: { name: 'Goblin', ability_modifiers: { dexterity: 2 } }, initiative: 0 },
468
+ { id: 'wizard', character: { name: 'Wizard', ability_modifiers: { dexterity: 0 } }, initiative: 0 }
469
+ ];
470
+
471
+ // Roll initiative!
472
+ const { sortedCombatants } = roller.rollInitiativeForAll(combatants);
473
+
474
+ // Display turn order
475
+ console.log('=== INITIATIVE ORDER ===');
476
+ roller.getInitiativeOrder(sortedCombatants).forEach(line => console.log(line));
477
+
478
+ // Run combat rounds
479
+ let currentRound = 1;
480
+ let currentTurnIndex = 0;
481
+
482
+ while (combatants.length > 1) {
483
+ const { combatant, index, isNewRound } = roller.getNextCombatant(sortedCombatants, currentTurnIndex);
484
+
485
+ if (isNewRound) {
486
+ currentRound++;
487
+ console.log(`\n--- ROUND ${currentRound} ---`);
488
+ }
489
+
490
+ console.log(`${combatant.character.name}'s turn...`);
491
+
492
+ // Combat logic here (attacks, spells, etc.)
493
+ // Use DiceRoller for all dice rolling
494
+ const attackRoll = DiceRoller.rollD20();
495
+
496
+ currentTurnIndex = index;
497
+ }
498
+ ```
499
+
500
+ ---
501
+
502
+ ## Seeded RNG in Combat Simulations
503
+
504
+ Combat simulations use seeded RNG at two layers: **enemy generation** (via `SeededRNG`) and **combat dice rolls** (via `SeededDiceRoller`). Understanding how seeds flow through the system is essential for writing reproducible balance tests and simulation pipelines.
505
+
506
+ ### Two Layers of Seeded Randomness
507
+
508
+ | Layer | Class | Purpose | Seed Source |
509
+ |-------|-------|---------|-------------|
510
+ | Enemy generation | `SeededRNG` | Deterministic character stats, equipment, spell selection | User-provided seed string |
511
+ | Combat dice rolls | `SeededDiceRoller` | Deterministic attack rolls, damage, saving throws, initiative | Derived per simulation run |
512
+
513
+ These are independent — the combat roller does not share state with the generation RNG. A single `SeededRNG` instance is used during enemy generation (stats, spells, equipment), then discarded. A fresh `SeededDiceRoller` is created for each simulation run.
514
+
515
+ ### How Seeds Flow in a Simulation
516
+
517
+ The `CombatSimulator` manages seeding across multiple runs. Given a `baseSeed` and `runCount`, each run gets a unique deterministic seed:
518
+
519
+ ```
520
+ baseSeed: "balance-test-1"
521
+ runCount: 1000
522
+
523
+ Run 0 → seed: "balance-test-1-0"
524
+ Run 1 → seed: "balance-test-1-1"
525
+ Run 2 → seed: "balance-test-1-2"
526
+ ...
527
+ Run 999 → seed: "balance-test-1-999"
528
+ ```
529
+
530
+ The seed is formatted as `${baseSeed}-${runIndex}`. Each seed produces a completely independent `SeededDiceRoller` instance, ensuring no state leaks between runs.
531
+
532
+ ### Determinism Guarantees
533
+
534
+ **Same base seed + same config = identical results, always.**
535
+
536
+ This holds because the entire system is deterministic:
537
+ - `SeededRNG` uses MurmurHash V3 via `deriveSeed()` + `hashSeedToFloat()` with a stateful counter
538
+ - `SeededDiceRoller` wraps `SeededRNG` — same seed, same call sequence = same rolls
539
+ - `CombatEngine` receives the roller via constructor injection, passing it to `AttackResolver`, `InitiativeRoller`, and `SpellCaster`
540
+ - `AICombatRunner` creates a fresh `CombatEngine` per run with the run-specific roller
541
+ - AI decisions are deterministic given the same combatant states (no random AI choices)
542
+
543
+ ```typescript
544
+ // Proof of determinism
545
+ const simulator = new CombatSimulator();
546
+ const resultsA = simulator.run(party, enemies, { runCount: 500, baseSeed: 'test-42', aiConfig });
547
+ const resultsB = simulator.run(party, enemies, { runCount: 500, baseSeed: 'test-42', aiConfig });
548
+
549
+ // These are identical — same summary, same per-combatant metrics, same histograms
550
+ console.log(resultsA.summary.playerWinRate === resultsB.summary.playerWinRate); // true
551
+ ```
552
+
553
+ ### What Changes Between Seeds
554
+
555
+ Changing any of these produces different results:
556
+ - **`baseSeed`** — different random rolls across all runs
557
+ - **`runCount`** — more/fewer data points, different aggregate statistics
558
+ - **`aiConfig`** — different AI decisions change combat flow
559
+ - **`combatConfig.maxTurnsBeforeDraw`** — changes how stalemates resolve
560
+
561
+ ```typescript
562
+ // Different seed → different results
563
+ const results1 = simulator.run(party, enemies, { runCount: 500, baseSeed: 'seed-A', aiConfig });
564
+ const results2 = simulator.run(party, enemies, { runCount: 500, baseSeed: 'seed-B', aiConfig });
565
+ console.log(results1.summary.playerWinRate === results2.summary.playerWinRate); // almost certainly false
566
+ ```
567
+
568
+ ### Seed Strategy for Balance Testing
569
+
570
+ When testing balance across multiple configurations (e.g., parameter sweeps), use a shared base seed so the only variable is the parameter being tested:
571
+
572
+ ```typescript
573
+ import { CombatSimulator, ParameterSweep } from 'playlist-data-engine';
574
+
575
+ // ParameterSweep uses this strategy internally:
576
+ // Each data point gets seed `${baseSeed}-${parameterValue}`
577
+ // This ensures each CR level is tested with independent but reproducible RNG
578
+
579
+ const sweep = new ParameterSweep();
580
+ const results = sweep.sweep(
581
+ party, baseEnemy,
582
+ {
583
+ variable: 'cr',
584
+ range: { min: 1, max: 10, step: 1 },
585
+ simulationsPerPoint: 500,
586
+ aiConfig: { playerStyle: 'normal', enemyStyle: 'aggressive' },
587
+ baseSeed: 'cr-sweep-v1',
588
+ }
589
+ );
590
+ // Each CR value (1 through 10) gets 500 runs with deterministic seeds
591
+ ```
592
+
593
+ ### Relationship Between SeededRNG and SeededDiceRoller
594
+
595
+ `SeededRNG` is the low-level PRNG (pseudorandom number generator). `SeededDiceRoller` is a D&D-specific wrapper that translates RNG output into game mechanics:
596
+
597
+ ```
598
+ SeededRNG SeededDiceRoller
599
+ ───────── ─────────────────
600
+ random() → 0.0-1.0 rollDie(20) → 1-20
601
+ randomInt(a,b) → integer rollD20() → 1-20
602
+ randomChoice() → element rollWithAdvantage() → {roll1, roll2, result}
603
+ weightedChoice()→ element calculateDamage() → {total, rolls, modifier}
604
+ shuffle() → shuffled rollSavingThrow() → d20 + mod + prof
605
+ ```
606
+
607
+ `SeededDiceRoller` is **instance-based** (not static like `DiceRoller`). Each simulation run creates its own instance with its own `SeededRNG` state. This is what makes per-run isolation possible.
608
+
609
+ ### Injecting a Seeded Roller into CombatEngine
610
+
611
+ For live combat (random dice), use `CombatEngine` with no roller — it uses `Math.random()` via the static `DiceRoller`:
612
+
613
+ ```typescript
614
+ const engine = new CombatEngine({ maxTurnsBeforeDraw: 50 });
615
+ // Uses Math.random() — non-deterministic
616
+ ```
617
+
618
+ For simulation combat (deterministic dice), inject a `SeededDiceRoller`:
619
+
620
+ ```typescript
621
+ import { createSeededRoller } from 'playlist-data-engine';
622
+
623
+ const engine = new CombatEngine({}, createSeededRoller('my-seed'));
624
+ // Every d20 roll, damage roll, and saving throw is deterministic
625
+ ```
626
+
627
+ The injected roller flows to all combat subsystems:
628
+
629
+ ```
630
+ CombatEngine
631
+ ├── AttackResolver (receives roller) → attack rolls, damage rolls
632
+ ├── InitiativeRoller (receives roller) → initiative rolls
633
+ └── SpellCaster (receives roller) → spell attack rolls, saving throws
634
+ ```
635
+
636
+ ### Full Simulation Pipeline with Seeding
637
+
638
+ ```typescript
639
+ import {
640
+ CombatSimulator,
641
+ createSeededRoller,
642
+ SeededRNG,
643
+ EnemyGenerator,
644
+ } from 'playlist-data-engine';
645
+
646
+ // Step 1: Generate enemies deterministically (SeededRNG for generation)
647
+ const enemyGen = new EnemyGenerator();
648
+ const enemy = enemyGen.generate({
649
+ seed: 'encounter-v3',
650
+ cr: 5,
651
+ rarity: 'elite',
652
+ category: 'humanoid',
653
+ archetype: 'brute',
654
+ });
655
+
656
+ // Step 2: Run simulations (SeededDiceRoller per run, managed by CombatSimulator)
657
+ const simulator = new CombatSimulator();
658
+ const results = simulator.run(
659
+ party, [enemy],
660
+ {
661
+ runCount: 1000,
662
+ baseSeed: 'balance-test-3', // Each run gets "balance-test-3-0" through "balance-test-3-999"
663
+ aiConfig: {
664
+ playerStyle: 'normal',
665
+ enemyStyle: 'aggressive',
666
+ },
667
+ onProgress: (done, total) => {
668
+ console.log(`Progress: ${done}/${total} (${((done/total)*100).toFixed(1)}%)`);
669
+ },
670
+ }
671
+ );
672
+
673
+ console.log(`Win rate: ${(results.summary.playerWinRate * 100).toFixed(1)}%`);
674
+ console.log(`Avg rounds: ${results.summary.averageRounds.toFixed(1)}`);
675
+ console.log(`Player deaths: ${results.summary.totalPlayerDeaths} across ${results.summary.totalRuns} runs`);
676
+ ```
677
+
678
+ **Key point:** The enemy generation seed (`encounter-v3`) and the simulation base seed (`balance-test-3`) are independent. Changing the simulation seed does not change which enemies are generated — only how the combat dice fall. This separation lets you test the same encounter composition across many different combat scenarios.
679
+
680
+ ---
681
+
682
+ ## See Also
683
+
684
+ - [DATA_ENGINE_REFERENCE.md](../DATA_ENGINE_REFERENCE.md) - Complete API reference
685
+ - [USAGE_IN_OTHER_PROJECTS.md](../USAGE_IN_OTHER_PROJECTS.md) - Usage examples
686
+ - [COMBAT_SYSTEM.md](COMBAT_SYSTEM.md) - Combat system (uses seeded dice for simulations)
687
+ - [ENEMY_GENERATION.md](ENEMY_GENERATION.md) - Balance validation and simulation-based analysis