shufflecom-calculations 1.3.2 → 1.3.4

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,148 @@
1
+ import { createHmac } from 'crypto';
2
+ import BigNumber from 'bignumber.js';
3
+ import { WHEEL_MULTIPLIERS, Wheel, WheelRiskLevel, WheelSegments } from './wheel';
4
+
5
+ interface ProvablyFair {
6
+ serverSeed: string;
7
+ clientSeed: string;
8
+ nonce: string;
9
+ segments: WheelSegments;
10
+ risk: WheelRiskLevel;
11
+ resultSegment: number;
12
+ multiplier: number;
13
+ }
14
+
15
+ describe('Wheel', () => {
16
+ it('edge for all segments and risk levels should be constant at 1%', () => {
17
+ Object.values(WheelSegments).forEach((segment: WheelSegments) => {
18
+ Object.values(WheelRiskLevel).forEach((risk: WheelRiskLevel) => {
19
+ const multiplierArr = WHEEL_MULTIPLIERS[segment][risk];
20
+ const returnToPlayer = multiplierArr.reduce((acc, curr) => acc.plus(curr), BigNumber(0)).dividedBy(multiplierArr.length);
21
+
22
+ expect(returnToPlayer.toString()).toEqual('0.99');
23
+ });
24
+ });
25
+ });
26
+
27
+ it('be able generate provably fair results', () => {
28
+ const gameSeeds: Array<ProvablyFair> = [
29
+ {
30
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
31
+ clientSeed: 'pf4jl5q97q',
32
+ nonce: '2',
33
+ segments: WheelSegments.TEN,
34
+ risk: WheelRiskLevel.LOW,
35
+ resultSegment: 5,
36
+ multiplier: 1.2,
37
+ },
38
+ {
39
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
40
+ clientSeed: 'pf4jl5q97q',
41
+ nonce: '3',
42
+ segments: WheelSegments.TEN,
43
+ risk: WheelRiskLevel.LOW,
44
+ resultSegment: 7,
45
+ multiplier: 1.2,
46
+ },
47
+ {
48
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
49
+ clientSeed: 'fasd',
50
+ nonce: '3',
51
+ segments: WheelSegments.TEN,
52
+ risk: WheelRiskLevel.LOW,
53
+ resultSegment: 2,
54
+ multiplier: 1.2,
55
+ },
56
+ {
57
+ serverSeed: 'FDL:SJ#33f9asdfasd',
58
+ clientSeed: 'pf4jl5q97q',
59
+ nonce: '2',
60
+ segments: WheelSegments.TEN,
61
+ risk: WheelRiskLevel.LOW,
62
+ resultSegment: 2,
63
+ multiplier: 1.2,
64
+ },
65
+
66
+ {
67
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
68
+ clientSeed: 'fasd',
69
+ nonce: '3',
70
+ segments: WheelSegments.TWENTY,
71
+ risk: WheelRiskLevel.HIGH,
72
+ resultSegment: 4,
73
+ multiplier: 0,
74
+ },
75
+ {
76
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
77
+ clientSeed: 'fasd',
78
+ nonce: '4',
79
+ segments: WheelSegments.THIRTY,
80
+ risk: WheelRiskLevel.LOW,
81
+ resultSegment: 21,
82
+ multiplier: 1.2,
83
+ },
84
+ {
85
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
86
+ clientSeed: 'fasd',
87
+ nonce: '5',
88
+ segments: WheelSegments.FORTY,
89
+ risk: WheelRiskLevel.MEDIUM,
90
+ resultSegment: 4,
91
+ multiplier: 3,
92
+ },
93
+ {
94
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
95
+ clientSeed: 'fasd',
96
+ nonce: '3',
97
+ segments: WheelSegments.FIFTY,
98
+ risk: WheelRiskLevel.HIGH,
99
+ resultSegment: 10,
100
+ multiplier: 0,
101
+ },
102
+ {
103
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
104
+ clientSeed: 'fasd',
105
+ nonce: '3',
106
+ segments: WheelSegments.THIRTY,
107
+ risk: WheelRiskLevel.MEDIUM,
108
+ resultSegment: 6,
109
+ multiplier: 2,
110
+ },
111
+ {
112
+ serverSeed: '9b0499f7abbebf3ea889e3f958d1bc2d77c514792143d07dc66378711f7eb313',
113
+ clientSeed: 'fasd',
114
+ nonce: '43',
115
+ segments: WheelSegments.FORTY,
116
+ risk: WheelRiskLevel.HIGH,
117
+ resultSegment: 39,
118
+ multiplier: 39.6,
119
+ },
120
+ ];
121
+ gameSeeds.forEach((e) => {
122
+ const resultHex = generateDigestHex(e);
123
+ const { multiplier, resultSegment } = Wheel.getResult(resultHex[0], e.segments, e.risk);
124
+ expect(multiplier.toNumber()).toEqual(e.multiplier);
125
+ expect(resultSegment).toEqual(e.resultSegment);
126
+ });
127
+ });
128
+ });
129
+
130
+ function generateDigestHex({
131
+ serverSeed,
132
+ clientSeed,
133
+ nonce,
134
+ rounds = 2,
135
+ }: {
136
+ serverSeed: string;
137
+ clientSeed: string;
138
+ nonce: string;
139
+ rounds?: number;
140
+ }): string[] {
141
+ const results = Array.from(Array(rounds).keys()).map((round) => {
142
+ const hmac = createHmac('sha256', serverSeed);
143
+ hmac.update(`${clientSeed}:${nonce}:${round}`);
144
+ return hmac.digest('hex');
145
+ });
146
+
147
+ return results;
148
+ }
@@ -0,0 +1,94 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { hexToBytes } from '../utils/hex-to-bytes';
3
+
4
+ const BYTE_TOTAL = new BigNumber(256);
5
+
6
+ export enum WheelRiskLevel {
7
+ LOW = 'LOW',
8
+ MEDIUM = 'MEDIUM',
9
+ HIGH = 'HIGH',
10
+ }
11
+
12
+ // can't map to number because using numbers in the enum doesn't work well with graphql enums
13
+ export enum WheelSegments {
14
+ TEN = 'TEN',
15
+ TWENTY = 'TWENTY',
16
+ THIRTY = 'THIRTY',
17
+ FORTY = 'FORTY',
18
+ FIFTY = 'FIFTY',
19
+ }
20
+
21
+ export const WHEEL_MULTIPLIERS: Record<WheelSegments, Record<WheelRiskLevel, number[]>> = {
22
+ [WheelSegments.TEN]: {
23
+ [WheelRiskLevel.LOW]: [1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0],
24
+ [WheelRiskLevel.MEDIUM]: [0, 1.9, 0, 1.5, 0, 2, 0, 1.5, 0, 3],
25
+ [WheelRiskLevel.HIGH]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 9.9],
26
+ },
27
+ [WheelSegments.TWENTY]: {
28
+ [WheelRiskLevel.LOW]: [1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0],
29
+ [WheelRiskLevel.MEDIUM]: [1.5, 0, 2, 0, 1.8, 0, 2, 0, 2, 0, 1.5, 0, 2, 0, 2, 0, 3, 0, 2, 0],
30
+ [WheelRiskLevel.HIGH]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19.8],
31
+ },
32
+ [WheelSegments.THIRTY]: {
33
+ [WheelRiskLevel.LOW]: [
34
+ 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
35
+ ],
36
+ [WheelRiskLevel.MEDIUM]: [1.5, 0, 2, 0, 1.5, 0, 2, 0, 3, 0, 1.7, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 4, 0, 1.5, 0, 2, 0],
37
+ [WheelRiskLevel.HIGH]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29.7],
38
+ },
39
+ [WheelSegments.FORTY]: {
40
+ [WheelRiskLevel.LOW]: [
41
+ 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2,
42
+ 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
43
+ ],
44
+ [WheelRiskLevel.MEDIUM]: [
45
+ 2, 0, 1.5, 0, 3, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0, 2, 0, 1.5, 0, 1.6, 0, 1.5, 0, 3, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0, 2, 0, 1.5, 0,
46
+ ],
47
+ [WheelRiskLevel.HIGH]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 39.6],
48
+ },
49
+ [WheelSegments.FIFTY]: {
50
+ [WheelRiskLevel.LOW]: [
51
+ 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2,
52
+ 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0, 1.5, 1.2, 1.2, 1.2, 0, 1.2, 1.2, 1.2, 1.2, 0,
53
+ ],
54
+ [WheelRiskLevel.MEDIUM]: [
55
+ 1.5, 0, 2, 0, 1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 3, 0, 1.5, 0, 2, 0, 1.5, 0, 2, 0, 1.5, 0, 5, 0,
56
+ 1.5, 0, 2, 0, 1.5, 0,
57
+ ],
58
+ [WheelRiskLevel.HIGH]: [
59
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49.5,
60
+ ],
61
+ },
62
+ };
63
+
64
+ export const WHEEL_SEGMENTS_TO_NUMBER: Record<WheelSegments, number> = {
65
+ [WheelSegments.TEN]: 10,
66
+ [WheelSegments.TWENTY]: 20,
67
+ [WheelSegments.THIRTY]: 30,
68
+ [WheelSegments.FORTY]: 40,
69
+ [WheelSegments.FIFTY]: 50,
70
+ };
71
+
72
+ export class Wheel {
73
+ static getResult(hexStr: string, segments: WheelSegments, risk: WheelRiskLevel): { resultSegment: number; multiplier: BigNumber } {
74
+ const resultSegment = this.getRandomNumberFromHexStr(hexStr).multipliedBy(WHEEL_SEGMENTS_TO_NUMBER[segments]).integerValue(BigNumber.ROUND_DOWN).toNumber();
75
+ const multiplier = BigNumber(WHEEL_MULTIPLIERS[segments][risk][resultSegment]);
76
+ return { resultSegment, multiplier };
77
+ }
78
+
79
+ // move to a separate file
80
+ private static getRandomNumberFromHexStr(hexStr: string): BigNumber {
81
+ const gameResultBytes = hexToBytes(hexStr);
82
+
83
+ let result = new BigNumber(0);
84
+
85
+ // Only use the first 4 bytes to get the random number
86
+ for (let i = 0; i < 4; i++) {
87
+ const value = gameResultBytes[i];
88
+
89
+ result = result.plus(new BigNumber(value).dividedBy(BYTE_TOTAL.pow(i + 1)));
90
+ }
91
+
92
+ return result;
93
+ }
94
+ }