@swrpg-online/dice 0.5.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.
package/src/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/src/types.ts ADDED
@@ -0,0 +1,30 @@
1
+ export type DicePool = {
2
+ boostDice?: number;
3
+ abilityDice?: number;
4
+ proficiencyDice?: number;
5
+ setBackDice?: number;
6
+ difficultyDice?: number;
7
+ challengeDice?: number;
8
+ };
9
+
10
+ export type DiceResult = {
11
+ successes: number;
12
+ failures: number;
13
+ advantages: number;
14
+ threats: number;
15
+ triumphs: number;
16
+ despair: number;
17
+ };
18
+
19
+ export type DieType = 'boost' | 'ability' | 'proficiency' | 'setback' | 'difficulty' | 'challenge';
20
+
21
+ export type DetailedDieResult = {
22
+ type: DieType;
23
+ roll: number;
24
+ result: DiceResult;
25
+ };
26
+
27
+ export type RollResult = {
28
+ results: DetailedDieResult[];
29
+ summary: DiceResult;
30
+ };
@@ -0,0 +1,351 @@
1
+ import { roll } from '../src/dice';
2
+ import { DicePool, RollResult } from '../src/types';
3
+
4
+ // Mock Math.random for deterministic tests
5
+ const mockMathRandom = (value: number) => {
6
+ const originalRandom = Math.random;
7
+ Math.random = jest.fn().mockReturnValue(value);
8
+ return () => {
9
+ Math.random = originalRandom;
10
+ };
11
+ };
12
+
13
+ describe('SWRPG Dice Rolling', () => {
14
+ afterEach(() => {
15
+ jest.restoreAllMocks();
16
+ });
17
+
18
+ describe('Detailed Roll Breakdown', () => {
19
+ test('returns detailed results for each die', () => {
20
+ const cleanup = mockMathRandom(0.5); // mid-range roll
21
+ const pool: DicePool = {
22
+ boostDice: 1,
23
+ abilityDice: 1
24
+ };
25
+
26
+ const result = roll(pool);
27
+
28
+ expect(result).toHaveProperty('results');
29
+ expect(result).toHaveProperty('summary');
30
+ expect(Array.isArray(result.results)).toBe(true);
31
+ expect(result.results).toHaveLength(2);
32
+
33
+ // Check structure of detailed results
34
+ result.results.forEach(dieResult => {
35
+ expect(dieResult).toHaveProperty('type');
36
+ expect(dieResult).toHaveProperty('roll');
37
+ expect(dieResult).toHaveProperty('result');
38
+ expect(['boost', 'ability']).toContain(dieResult.type);
39
+ expect(typeof dieResult.roll).toBe('number');
40
+ });
41
+
42
+ cleanup();
43
+ });
44
+
45
+ test('properly identifies die types', () => {
46
+ const cleanup = mockMathRandom(0.5);
47
+ const pool: DicePool = {
48
+ boostDice: 1,
49
+ abilityDice: 1,
50
+ proficiencyDice: 1,
51
+ setBackDice: 1,
52
+ difficultyDice: 1,
53
+ challengeDice: 1
54
+ };
55
+
56
+ const result = roll(pool);
57
+ const dieTypes = result.results.map(r => r.type);
58
+
59
+ expect(dieTypes).toContain('boost');
60
+ expect(dieTypes).toContain('ability');
61
+ expect(dieTypes).toContain('proficiency');
62
+ expect(dieTypes).toContain('setback');
63
+ expect(dieTypes).toContain('difficulty');
64
+ expect(dieTypes).toContain('challenge');
65
+
66
+ cleanup();
67
+ });
68
+ });
69
+
70
+ describe('Individual Die Results', () => {
71
+ describe('Boost Die (d6)', () => {
72
+ test.each([
73
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
74
+ [2, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
75
+ [3, { successes: 1, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
76
+ [4, { successes: 1, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
77
+ [5, { successes: 0, failures: 0, advantages: 2, threats: 0, triumphs: 0, despair: 0 }],
78
+ [6, { successes: 0, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }]
79
+ ])('face value %i should return correct results', (value, expected) => {
80
+ const cleanup = mockMathRandom((value - 1) / 6);
81
+ const pool: DicePool = { boostDice: 1 };
82
+ expect(roll(pool).summary).toEqual(expected);
83
+ cleanup();
84
+ });
85
+ });
86
+
87
+ describe('Ability Die (d8)', () => {
88
+ test.each([
89
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
90
+ [2, { successes: 1, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
91
+ [3, { successes: 1, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
92
+ [4, { successes: 2, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
93
+ [5, { successes: 0, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
94
+ [6, { successes: 0, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
95
+ [7, { successes: 1, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
96
+ [8, { successes: 0, failures: 0, advantages: 2, threats: 0, triumphs: 0, despair: 0 }]
97
+ ])('face value %i should return correct results', (value, expected) => {
98
+ const cleanup = mockMathRandom((value - 1) / 8);
99
+ const pool: DicePool = { abilityDice: 1 };
100
+ expect(roll(pool).summary).toEqual(expected);
101
+ cleanup();
102
+ });
103
+ });
104
+
105
+ describe('Proficiency Die (d12)', () => {
106
+ test.each([
107
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
108
+ [2, { successes: 1, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
109
+ [3, { successes: 1, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
110
+ [4, { successes: 2, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
111
+ [5, { successes: 2, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
112
+ [6, { successes: 0, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
113
+ [7, { successes: 1, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
114
+ [8, { successes: 1, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
115
+ [9, { successes: 1, failures: 0, advantages: 1, threats: 0, triumphs: 0, despair: 0 }],
116
+ [10, { successes: 0, failures: 0, advantages: 2, threats: 0, triumphs: 0, despair: 0 }],
117
+ [11, { successes: 0, failures: 0, advantages: 2, threats: 0, triumphs: 0, despair: 0 }],
118
+ [12, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 1, despair: 0 }]
119
+ ])('face value %i should return correct results', (value, expected) => {
120
+ const cleanup = mockMathRandom((value - 1) / 12);
121
+ const pool: DicePool = { proficiencyDice: 1 };
122
+ expect(roll(pool).summary).toEqual(expected);
123
+ cleanup();
124
+ });
125
+ });
126
+
127
+ describe('Setback Die (d6)', () => {
128
+ test.each([
129
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
130
+ [2, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
131
+ [3, { successes: 0, failures: 1, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
132
+ [4, { successes: 0, failures: 1, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
133
+ [5, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
134
+ [6, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }]
135
+ ])('face value %i should return correct results', (value, expected) => {
136
+ const cleanup = mockMathRandom((value - 1) / 6);
137
+ const pool: DicePool = { setBackDice: 1 };
138
+ expect(roll(pool).summary).toEqual(expected);
139
+ cleanup();
140
+ });
141
+ });
142
+
143
+ describe('Difficulty Die (d8)', () => {
144
+ test.each([
145
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
146
+ [2, { successes: 0, failures: 1, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
147
+ [3, { successes: 0, failures: 2, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
148
+ [4, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
149
+ [5, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
150
+ [6, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
151
+ [7, { successes: 0, failures: 0, advantages: 0, threats: 2, triumphs: 0, despair: 0 }],
152
+ [8, { successes: 0, failures: 1, advantages: 0, threats: 1, triumphs: 0, despair: 0 }]
153
+ ])('face value %i should return correct results', (value, expected) => {
154
+ const cleanup = mockMathRandom((value - 1) / 8);
155
+ const pool: DicePool = { difficultyDice: 1 };
156
+ expect(roll(pool).summary).toEqual(expected);
157
+ cleanup();
158
+ });
159
+ });
160
+
161
+ describe('Challenge Die (d12)', () => {
162
+ test.each([
163
+ [1, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
164
+ [2, { successes: 0, failures: 1, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
165
+ [3, { successes: 0, failures: 1, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
166
+ [4, { successes: 0, failures: 2, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
167
+ [5, { successes: 0, failures: 2, advantages: 0, threats: 0, triumphs: 0, despair: 0 }],
168
+ [6, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
169
+ [7, { successes: 0, failures: 0, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
170
+ [8, { successes: 0, failures: 1, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
171
+ [9, { successes: 0, failures: 1, advantages: 0, threats: 1, triumphs: 0, despair: 0 }],
172
+ [10, { successes: 0, failures: 0, advantages: 0, threats: 2, triumphs: 0, despair: 0 }],
173
+ [11, { successes: 0, failures: 0, advantages: 0, threats: 2, triumphs: 0, despair: 0 }],
174
+ [12, { successes: 0, failures: 0, advantages: 0, threats: 0, triumphs: 0, despair: 1 }]
175
+ ])('face value %i should return correct results', (value, expected) => {
176
+ const cleanup = mockMathRandom((value - 1) / 12);
177
+ const pool: DicePool = { challengeDice: 1 };
178
+ expect(roll(pool).summary).toEqual(expected);
179
+ cleanup();
180
+ });
181
+ });
182
+ });
183
+
184
+ describe('Edge Cases', () => {
185
+ test('negative numbers default to 0', () => {
186
+ const pool: DicePool = {
187
+ boostDice: -1,
188
+ abilityDice: -2,
189
+ proficiencyDice: -1,
190
+ setBackDice: -3,
191
+ difficultyDice: -2,
192
+ challengeDice: -1
193
+ };
194
+ const expected = {
195
+ successes: 0,
196
+ failures: 0,
197
+ advantages: 0,
198
+ threats: 0,
199
+ triumphs: 0,
200
+ despair: 0
201
+ };
202
+ expect(roll(pool).summary).toEqual(expected);
203
+ });
204
+
205
+ test('undefined values default to 0', () => {
206
+ const pool: DicePool = {
207
+ boostDice: undefined,
208
+ abilityDice: undefined,
209
+ proficiencyDice: undefined,
210
+ setBackDice: undefined,
211
+ difficultyDice: undefined,
212
+ challengeDice: undefined
213
+ };
214
+ const expected = {
215
+ successes: 0,
216
+ failures: 0,
217
+ advantages: 0,
218
+ threats: 0,
219
+ triumphs: 0,
220
+ despair: 0
221
+ };
222
+ expect(roll(pool).summary).toEqual(expected);
223
+ });
224
+
225
+ test('empty pool returns zero results', () => {
226
+ const pool: DicePool = {};
227
+ const expected = {
228
+ successes: 0,
229
+ failures: 0,
230
+ advantages: 0,
231
+ threats: 0,
232
+ triumphs: 0,
233
+ despair: 0
234
+ };
235
+ expect(roll(pool).summary).toEqual(expected);
236
+ });
237
+ });
238
+
239
+ describe('Result Accumulation', () => {
240
+ test('successes and failures cancel out', () => {
241
+ // Mock to generate one success and one failure
242
+ const cleanup1 = mockMathRandom(1/8); // ability die success (face value 2)
243
+ const cleanup2 = mockMathRandom(1/8); // difficulty die failure (face value 2)
244
+
245
+ const pool: DicePool = {
246
+ abilityDice: 1,
247
+ difficultyDice: 1
248
+ };
249
+
250
+ const result = roll(pool);
251
+ expect(result.summary.successes).toBe(0);
252
+ expect(result.summary.failures).toBe(0);
253
+
254
+ cleanup1();
255
+ cleanup2();
256
+ });
257
+
258
+ test('advantages and threats accumulate independently', () => {
259
+ // Mock to generate advantages and threats
260
+ const cleanup1 = mockMathRandom(4/6); // boost die success + advantage
261
+ const cleanup2 = mockMathRandom(6/8); // difficulty die threat
262
+
263
+ const pool: DicePool = {
264
+ boostDice: 1,
265
+ difficultyDice: 1
266
+ };
267
+
268
+ const result = roll(pool);
269
+ expect(result.summary.advantages).toBeGreaterThan(0);
270
+ expect(result.summary.threats).toBeGreaterThan(0);
271
+
272
+ cleanup1();
273
+ cleanup2();
274
+ });
275
+
276
+ test('triumphs and despair count independently', () => {
277
+ // Mock to generate one triumph and one despair
278
+ const cleanup1 = mockMathRandom(11/12); // proficiency die triumph
279
+ const cleanup2 = mockMathRandom(11/12); // challenge die despair
280
+
281
+ const pool: DicePool = {
282
+ proficiencyDice: 1,
283
+ challengeDice: 1
284
+ };
285
+
286
+ const result = roll(pool);
287
+ expect(result.summary.triumphs).toBe(1);
288
+ expect(result.summary.despair).toBe(1);
289
+
290
+ cleanup1();
291
+ cleanup2();
292
+ });
293
+ });
294
+
295
+ describe('Complex Combinations', () => {
296
+ test('realistic skill check - standard difficulty', () => {
297
+ const pool: DicePool = {
298
+ abilityDice: 2,
299
+ proficiencyDice: 1,
300
+ difficultyDice: 2
301
+ };
302
+
303
+ const result = roll(pool);
304
+
305
+ // Verify result structure
306
+ expect(result.summary).toHaveProperty('successes');
307
+ expect(result.summary).toHaveProperty('failures');
308
+ expect(result.summary).toHaveProperty('advantages');
309
+ expect(result.summary).toHaveProperty('threats');
310
+ expect(result.summary).toHaveProperty('triumphs');
311
+ expect(result.summary).toHaveProperty('despair');
312
+
313
+ // Verify ranges
314
+ expect(result.summary.successes).toBeGreaterThanOrEqual(0);
315
+ expect(result.summary.failures).toBeGreaterThanOrEqual(0);
316
+ expect(result.summary.advantages).toBeGreaterThanOrEqual(0);
317
+ expect(result.summary.threats).toBeGreaterThanOrEqual(0);
318
+ expect(result.summary.triumphs).toBeGreaterThanOrEqual(0);
319
+ expect(result.summary.despair).toBeGreaterThanOrEqual(0);
320
+ });
321
+
322
+ test('opposed check - combat scenario', () => {
323
+ const pool: DicePool = {
324
+ abilityDice: 1,
325
+ proficiencyDice: 1,
326
+ boostDice: 1,
327
+ difficultyDice: 1,
328
+ challengeDice: 1,
329
+ setBackDice: 1
330
+ };
331
+
332
+ const result = roll(pool);
333
+
334
+ // Verify ranges
335
+ expect(result.summary.successes).toBeGreaterThanOrEqual(0);
336
+ expect(result.summary.failures).toBeGreaterThanOrEqual(0);
337
+ expect(result.summary.advantages).toBeGreaterThanOrEqual(0);
338
+ expect(result.summary.threats).toBeGreaterThanOrEqual(0);
339
+ expect(result.summary.triumphs).toBeGreaterThanOrEqual(0);
340
+ expect(result.summary.despair).toBeGreaterThanOrEqual(0);
341
+
342
+ // Check detailed results
343
+ expect(result.results).toHaveLength(6); // One result per die
344
+ result.results.forEach(dieResult => {
345
+ expect(dieResult).toHaveProperty('type');
346
+ expect(dieResult).toHaveProperty('roll');
347
+ expect(dieResult).toHaveProperty('result');
348
+ });
349
+ });
350
+ });
351
+ });
@@ -0,0 +1,86 @@
1
+ import {
2
+ createSkillCheck,
3
+ createCombatCheck,
4
+ createOpposedCheck,
5
+ createDifficultyPool
6
+ } from '../src/pools';
7
+
8
+ describe('Dice Pool Convenience Methods', () => {
9
+ describe('createSkillCheck', () => {
10
+ test('creates basic skill check pool', () => {
11
+ const pool = createSkillCheck(2, 1);
12
+ expect(pool).toEqual({
13
+ abilityDice: 2,
14
+ proficiencyDice: 1
15
+ });
16
+ });
17
+
18
+ test('creates pool with zero dice', () => {
19
+ const pool = createSkillCheck(0, 0);
20
+ expect(pool).toEqual({
21
+ abilityDice: 0,
22
+ proficiencyDice: 0
23
+ });
24
+ });
25
+ });
26
+
27
+ describe('createCombatCheck', () => {
28
+ test('creates combat check pool with boost', () => {
29
+ const pool = createCombatCheck(2, 1, 1);
30
+ expect(pool).toEqual({
31
+ abilityDice: 2,
32
+ proficiencyDice: 1,
33
+ boostDice: 1
34
+ });
35
+ });
36
+
37
+ test('creates combat check pool without boost', () => {
38
+ const pool = createCombatCheck(2, 1);
39
+ expect(pool).toEqual({
40
+ abilityDice: 2,
41
+ proficiencyDice: 1,
42
+ boostDice: 0
43
+ });
44
+ });
45
+ });
46
+
47
+ describe('createOpposedCheck', () => {
48
+ test('creates opposed check pool with challenge', () => {
49
+ const pool = createOpposedCheck(2, 1, 2, 1);
50
+ expect(pool).toEqual({
51
+ abilityDice: 2,
52
+ proficiencyDice: 1,
53
+ difficultyDice: 2,
54
+ challengeDice: 1
55
+ });
56
+ });
57
+
58
+ test('creates opposed check pool without challenge', () => {
59
+ const pool = createOpposedCheck(2, 1, 2);
60
+ expect(pool).toEqual({
61
+ abilityDice: 2,
62
+ proficiencyDice: 1,
63
+ difficultyDice: 2,
64
+ challengeDice: 0
65
+ });
66
+ });
67
+ });
68
+
69
+ describe('createDifficultyPool', () => {
70
+ test('creates difficulty pool with challenge', () => {
71
+ const pool = createDifficultyPool(2, 1);
72
+ expect(pool).toEqual({
73
+ difficultyDice: 2,
74
+ challengeDice: 1
75
+ });
76
+ });
77
+
78
+ test('creates difficulty pool without challenge', () => {
79
+ const pool = createDifficultyPool(2);
80
+ expect(pool).toEqual({
81
+ difficultyDice: 2,
82
+ challengeDice: 0
83
+ });
84
+ });
85
+ });
86
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2018",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": ["src/**/*"],
13
+ "exclude": ["node_modules", "**/*.test.ts"]
14
+ }