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.
- package/lib/games/blackjack.js +5 -5
- package/lib/games/blackjack.js.map +1 -1
- package/lib/games/cardGames.js +1 -1
- package/lib/games/cardGames.js.map +1 -1
- package/lib/games/crash.js.map +1 -1
- package/lib/games/dice.js +1 -1
- package/lib/games/dice.js.map +1 -1
- package/lib/games/hilo.js +2 -2
- package/lib/games/hilo.js.map +1 -1
- package/lib/games/keno.js +1 -1
- package/lib/games/keno.js.map +1 -1
- package/lib/games/limbo.js.map +1 -1
- package/lib/games/mines.js.map +1 -1
- package/lib/games/plinko.js +1 -1
- package/lib/games/plinko.js.map +1 -1
- package/lib/games/roulette.js +6 -6
- package/lib/games/roulette.js.map +1 -1
- package/lib/games/wheel.d.ts +22 -0
- package/lib/games/wheel.js +90 -0
- package/lib/games/wheel.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/chat-message.type.js +1 -1
- package/lib/utils/chat-message.type.js.map +1 -1
- package/lib/utils/dates-calculator.js.map +1 -1
- package/lib/utils/derive-vip-bonus.js.map +1 -1
- package/lib/utils/edge.js.map +1 -1
- package/lib/utils/factorial.js.map +1 -1
- package/lib/utils/getChatMessageLength.js.map +1 -1
- package/lib/utils/hex-to-bytes.js.map +1 -1
- package/lib/utils/uuid-converter.js.map +1 -1
- package/lib/utils/vip-bonus.type.js +2 -2
- package/lib/utils/vip-bonus.type.js.map +1 -1
- package/package.json +2 -2
- package/src/games/wheel.spec.ts +148 -0
- package/src/games/wheel.ts +94 -0
|
@@ -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
|
+
}
|