shufflecom-calculations 3.3.12 → 3.3.14
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/coinflip/coinflip-classic-progressive.d.ts +42 -0
- package/lib/games/coinflip/coinflip-classic-progressive.js +65 -0
- package/lib/games/coinflip/coinflip-classic-progressive.js.map +1 -0
- package/lib/games/coinflip/coinflip-classic.d.ts +18 -0
- package/lib/games/coinflip/coinflip-classic.js +25 -0
- package/lib/games/coinflip/coinflip-classic.js.map +1 -0
- package/lib/games/coinflip/coinflip-rules.d.ts +18 -0
- package/lib/games/coinflip/coinflip-rules.js +95 -0
- package/lib/games/coinflip/coinflip-rules.js.map +1 -0
- package/lib/games/coinflip/coinflip-target.d.ts +22 -0
- package/lib/games/coinflip/coinflip-target.js +28 -0
- package/lib/games/coinflip/coinflip-target.js.map +1 -0
- package/lib/games/coinflip/constants.d.ts +5 -0
- package/lib/games/coinflip/constants.js +10 -0
- package/lib/games/coinflip/constants.js.map +1 -0
- package/lib/games/fixed-rng.d.ts +15 -0
- package/lib/games/fixed-rng.js +24 -0
- package/lib/games/fixed-rng.js.map +1 -0
- package/lib/games/random-number-generator.interface.d.ts +12 -0
- package/lib/games/random-number-generator.interface.js +3 -0
- package/lib/games/random-number-generator.interface.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +8 -1
- package/lib/index.js.map +1 -1
- package/lib/sports/sports-bet.types.d.ts +2 -2
- package/lib/sports/sports.types.d.ts +10 -0
- package/lib/sports/sports.types.js +13 -2
- package/lib/sports/sports.types.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/chain-currency-display.js +4 -2
- package/lib/utils/chain-currency-display.js.map +1 -1
- package/package.json +2 -2
- package/src/games/coinflip/coinflip-classic-progressive.spec.ts +251 -0
- package/src/games/coinflip/coinflip-classic-progressive.ts +108 -0
- package/src/games/coinflip/coinflip-classic.spec.ts +46 -0
- package/src/games/coinflip/coinflip-classic.ts +37 -0
- package/src/games/coinflip/coinflip-ev.spec.ts +60 -0
- package/src/games/coinflip/coinflip-rules.spec.ts +193 -0
- package/src/games/coinflip/coinflip-rules.ts +104 -0
- package/src/games/coinflip/coinflip-target.spec.ts +73 -0
- package/src/games/coinflip/coinflip-target.ts +44 -0
- package/src/games/coinflip/constants.ts +6 -0
- package/src/games/fixed-rng.ts +25 -0
- package/src/games/random-number-generator.interface.ts +4 -0
- package/src/index.ts +5 -0
- package/src/sports/sports-bet.types.ts +2 -1
- package/src/sports/sports.types.ts +11 -0
- package/src/utils/chain-currency-display.ts +4 -2
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
import { FixedSequenceRng } from '../fixed-rng';
|
|
3
|
+
import {
|
|
4
|
+
CoinflipProgressiveCashoutAction,
|
|
5
|
+
CoinflipProgressiveCoinSelectionAction,
|
|
6
|
+
CoinflipProgressiveStartAction,
|
|
7
|
+
} from './coinflip-classic-progressive';
|
|
8
|
+
import { CoinflipRules } from './coinflip-rules';
|
|
9
|
+
import { CoinSide, MAX_FLIPS } from './constants';
|
|
10
|
+
|
|
11
|
+
const EDGE_BPS = 200;
|
|
12
|
+
|
|
13
|
+
describe('CoinflipRules.getFlipResults', () => {
|
|
14
|
+
it('maps 0 to heads and 1 to tails', () => {
|
|
15
|
+
const rng = new FixedSequenceRng([0, 1, 0, 1]);
|
|
16
|
+
expect(CoinflipRules.getFlipResults(rng, 4)).toEqual([CoinSide.HEADS, CoinSide.TAILS, CoinSide.HEADS, CoinSide.TAILS]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns exactly the requested number of flips', () => {
|
|
20
|
+
const rng = new FixedSequenceRng([0, 1, 0, 1, 0]);
|
|
21
|
+
expect(CoinflipRules.getFlipResults(rng, 2)).toHaveLength(2);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('throws when count is below 1', () => {
|
|
25
|
+
const rng = new FixedSequenceRng([0]);
|
|
26
|
+
expect(() => CoinflipRules.getFlipResults(rng, 0)).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('throws when count exceeds MAX_FLIPS', () => {
|
|
30
|
+
const rng = new FixedSequenceRng(Array(MAX_FLIPS + 1).fill(0));
|
|
31
|
+
expect(() => CoinflipRules.getFlipResults(rng, MAX_FLIPS + 1)).toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('throws when the generator returns fewer values than requested', () => {
|
|
35
|
+
const rng = new FixedSequenceRng([0]);
|
|
36
|
+
expect(() => CoinflipRules.getFlipResults(rng, 2)).toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('throws when the generator returns a value outside 0 and 1', () => {
|
|
40
|
+
const rng = new FixedSequenceRng([2]);
|
|
41
|
+
expect(() => CoinflipRules.getFlipResults(rng, 1)).toThrow();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('CoinflipRules.isFlipWin', () => {
|
|
46
|
+
it('returns true when the flip matches the selected side', () => {
|
|
47
|
+
expect(CoinflipRules.isFlipWin(CoinSide.HEADS, CoinSide.HEADS)).toBe(true);
|
|
48
|
+
expect(CoinflipRules.isFlipWin(CoinSide.TAILS, CoinSide.TAILS)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('returns false when the flip does not match', () => {
|
|
52
|
+
expect(CoinflipRules.isFlipWin(CoinSide.HEADS, CoinSide.TAILS)).toBe(false);
|
|
53
|
+
expect(CoinflipRules.isFlipWin(CoinSide.TAILS, CoinSide.HEADS)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('CoinflipRules.countSuccessfulFlips', () => {
|
|
58
|
+
it('counts only flips where result matches selectedSide', () => {
|
|
59
|
+
const flips = [
|
|
60
|
+
{ result: CoinSide.HEADS, selectedSide: CoinSide.HEADS },
|
|
61
|
+
{ result: CoinSide.TAILS, selectedSide: CoinSide.HEADS },
|
|
62
|
+
{ result: CoinSide.HEADS, selectedSide: CoinSide.HEADS },
|
|
63
|
+
{ result: CoinSide.HEADS, selectedSide: CoinSide.HEADS },
|
|
64
|
+
];
|
|
65
|
+
expect(CoinflipRules.countSuccessfulFlips(flips)).toBe(3);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('counts per-flip selectedSide independently', () => {
|
|
69
|
+
const flips = [
|
|
70
|
+
{ result: CoinSide.HEADS, selectedSide: CoinSide.HEADS },
|
|
71
|
+
{ result: CoinSide.TAILS, selectedSide: CoinSide.TAILS },
|
|
72
|
+
{ result: CoinSide.HEADS, selectedSide: CoinSide.TAILS },
|
|
73
|
+
];
|
|
74
|
+
expect(CoinflipRules.countSuccessfulFlips(flips)).toBe(2);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns 0 for an empty list', () => {
|
|
78
|
+
expect(CoinflipRules.countSuccessfulFlips([])).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('CoinflipRules.calculateClassicMultiplier', () => {
|
|
83
|
+
it.each([
|
|
84
|
+
[1, 1.96],
|
|
85
|
+
[2, 3.92],
|
|
86
|
+
[3, 7.84],
|
|
87
|
+
[10, 1003.52],
|
|
88
|
+
[20, 1027604.48],
|
|
89
|
+
])('%i successful flips pays %fx', (successfulFlips, expected) => {
|
|
90
|
+
expect(CoinflipRules.calculateClassicMultiplier(successfulFlips, EDGE_BPS).toNumber()).toBe(expected);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('throws when successful flips is below 1', () => {
|
|
94
|
+
expect(() => CoinflipRules.calculateClassicMultiplier(0, EDGE_BPS)).toThrow();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('throws when successful flips exceeds MAX_FLIPS', () => {
|
|
98
|
+
expect(() => CoinflipRules.calculateClassicMultiplier(MAX_FLIPS + 1, EDGE_BPS)).toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('CoinflipRules.calculateTargetMultiplier', () => {
|
|
103
|
+
it.each([
|
|
104
|
+
[1, 1, 1.96],
|
|
105
|
+
[2, 1, 1.306666667],
|
|
106
|
+
[2, 2, 3.92],
|
|
107
|
+
[3, 1, 1.12],
|
|
108
|
+
[3, 2, 1.96],
|
|
109
|
+
[4, 2, 1.425454545],
|
|
110
|
+
[5, 5, 31.36],
|
|
111
|
+
[10, 7, 5.701818182],
|
|
112
|
+
[20, 20, 1027604.48],
|
|
113
|
+
])('%i flips needing at least %i successes pays %fx', (flips, minSuccesses, expected) => {
|
|
114
|
+
expect(CoinflipRules.calculateTargetMultiplier(flips, minSuccesses, EDGE_BPS).decimalPlaces(9).toNumber()).toBe(expected);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('throws when flips is below 1', () => {
|
|
118
|
+
expect(() => CoinflipRules.calculateTargetMultiplier(0, 1, EDGE_BPS)).toThrow();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws when flips exceeds MAX_FLIPS', () => {
|
|
122
|
+
expect(() => CoinflipRules.calculateTargetMultiplier(MAX_FLIPS + 1, 1, EDGE_BPS)).toThrow();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('throws when minSuccesses is below 1', () => {
|
|
126
|
+
expect(() => CoinflipRules.calculateTargetMultiplier(5, 0, EDGE_BPS)).toThrow();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('throws when minSuccesses exceeds flips', () => {
|
|
130
|
+
expect(() => CoinflipRules.calculateTargetMultiplier(5, 6, EDGE_BPS)).toThrow();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('CoinflipRules.validateActiveProgressiveClassicGame', () => {
|
|
135
|
+
const start = (): CoinflipProgressiveStartAction => ({ phase: 'START' });
|
|
136
|
+
const win = (side = CoinSide.HEADS): CoinflipProgressiveCoinSelectionAction => ({
|
|
137
|
+
phase: 'COIN_SELECTION',
|
|
138
|
+
selectedSide: side,
|
|
139
|
+
flipResult: side,
|
|
140
|
+
multiplier: new BigNumber(0),
|
|
141
|
+
});
|
|
142
|
+
const cashout = (): CoinflipProgressiveCashoutAction => ({
|
|
143
|
+
phase: 'CASHOUT',
|
|
144
|
+
multiplier: new BigNumber(1.96),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('returns 0 for an empty action list', () => {
|
|
148
|
+
expect(CoinflipRules.validateActiveProgressiveClassicGame([])).toBe(0);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns 0 for a start-only history', () => {
|
|
152
|
+
expect(CoinflipRules.validateActiveProgressiveClassicGame([start()])).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('returns the number of winning selections', () => {
|
|
156
|
+
expect(CoinflipRules.validateActiveProgressiveClassicGame([start(), win(), win()])).toBe(2);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('throws when a CASHOUT action is present', () => {
|
|
160
|
+
expect(() => CoinflipRules.validateActiveProgressiveClassicGame([start(), win(), win(), cashout()])).toThrow(
|
|
161
|
+
'An active game cannot have a CASHOUT action',
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('throws when the history contains more than MAX_FLIPS flips', () => {
|
|
166
|
+
const tooManyWins = Array.from({ length: MAX_FLIPS + 1 }, () => win());
|
|
167
|
+
expect(() => CoinflipRules.validateActiveProgressiveClassicGame([start(), ...tooManyWins])).toThrow(
|
|
168
|
+
`Reconstructed flipsRevealed (${MAX_FLIPS + 1}) exceeds MAX_FLIPS (${MAX_FLIPS})`,
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('throws when the first action is not START', () => {
|
|
173
|
+
expect(() => CoinflipRules.validateActiveProgressiveClassicGame([win()])).toThrow('First action must be START');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('throws when a START action appears after the first position', () => {
|
|
177
|
+
expect(() => CoinflipRules.validateActiveProgressiveClassicGame([start(), win(), start()])).toThrow(
|
|
178
|
+
'START action can only appear as the first action',
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('throws when a COIN_SELECTION has mismatched flipResult', () => {
|
|
183
|
+
const badWin: CoinflipProgressiveCoinSelectionAction = {
|
|
184
|
+
phase: 'COIN_SELECTION',
|
|
185
|
+
selectedSide: CoinSide.HEADS,
|
|
186
|
+
flipResult: CoinSide.TAILS,
|
|
187
|
+
multiplier: new BigNumber(0),
|
|
188
|
+
};
|
|
189
|
+
expect(() => CoinflipRules.validateActiveProgressiveClassicGame([start(), badWin])).toThrow(
|
|
190
|
+
'COIN_SELECTION must have matching flipResult and selectedSide',
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
import { calculateEdgeMultiplier } from '../../utils/edge';
|
|
3
|
+
import { nCr } from '../../utils/ncr';
|
|
4
|
+
import { RandomNumberGenerator } from '../random-number-generator.interface';
|
|
5
|
+
import type { CoinflipClassicProgressiveGameAction } from './coinflip-classic-progressive';
|
|
6
|
+
import { CoinSide, MAX_FLIPS } from './constants';
|
|
7
|
+
|
|
8
|
+
function rngValueToCoinSide(n: number): CoinSide {
|
|
9
|
+
if (n === 0) return CoinSide.HEADS;
|
|
10
|
+
if (n === 1) return CoinSide.TAILS;
|
|
11
|
+
throw new Error(`Random number generator returned ${n}, expected 0 or 1`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class CoinflipRules {
|
|
15
|
+
public static getFlipResults(generator: RandomNumberGenerator, count: number): CoinSide[] {
|
|
16
|
+
if (count < 1 || count > MAX_FLIPS) {
|
|
17
|
+
throw new Error(`Flip count must be between 1 and ${MAX_FLIPS}, got ${count}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const randomNumbers = generator.getRandomIntsWithReplacement({ min: 0, max: 1, count });
|
|
21
|
+
|
|
22
|
+
if (randomNumbers.length !== count) {
|
|
23
|
+
throw new Error(`Random number generator returned ${randomNumbers.length} values, expected ${count}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return randomNumbers.map(rngValueToCoinSide);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static isFlipWin(selectedSide: CoinSide, result: CoinSide): boolean {
|
|
30
|
+
return selectedSide === result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static getFlipResultAtPosition(generator: RandomNumberGenerator, position: number): CoinSide {
|
|
34
|
+
const flips = CoinflipRules.getFlipResults(generator, position);
|
|
35
|
+
return flips[flips.length - 1] as CoinSide;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public static countSuccessfulFlips(flips: { result: CoinSide; selectedSide: CoinSide }[]): number {
|
|
39
|
+
return flips.filter(f => CoinflipRules.isFlipWin(f.selectedSide, f.result)).length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public static calculateClassicWinChance(successfulFlips: number): BigNumber {
|
|
43
|
+
return BigNumber(0.5).pow(successfulFlips);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public static calculateClassicMultiplier(successfulFlips: number, edgeBps: number): BigNumber {
|
|
47
|
+
if (successfulFlips < 1 || successfulFlips > MAX_FLIPS) {
|
|
48
|
+
throw new Error(`Successful flips must be between 1 and ${MAX_FLIPS}, got ${successfulFlips}`);
|
|
49
|
+
}
|
|
50
|
+
return calculateEdgeMultiplier(edgeBps).dividedBy(CoinflipRules.calculateClassicWinChance(successfulFlips));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public static calculateTargetWinChance(flips: number, minSuccesses: number): BigNumber {
|
|
54
|
+
const fixedOutcomeProbability = BigNumber(0.5).pow(flips);
|
|
55
|
+
let cumulativeProbability = BigNumber(0);
|
|
56
|
+
for (let k = minSuccesses; k <= flips; k++) {
|
|
57
|
+
cumulativeProbability = cumulativeProbability.plus(nCr(flips, k).multipliedBy(fixedOutcomeProbability));
|
|
58
|
+
}
|
|
59
|
+
return cumulativeProbability;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static calculateTargetMultiplier(flips: number, minSuccesses: number, edgeBps: number): BigNumber {
|
|
63
|
+
if (flips < 1 || flips > MAX_FLIPS) {
|
|
64
|
+
throw new Error(`Flips must be between 1 and ${MAX_FLIPS}, got ${flips}`);
|
|
65
|
+
}
|
|
66
|
+
if (minSuccesses < 1 || minSuccesses > flips) {
|
|
67
|
+
throw new Error(`Min successes must be between 1 and ${flips}, got ${minSuccesses}`);
|
|
68
|
+
}
|
|
69
|
+
return calculateEdgeMultiplier(edgeBps).dividedBy(CoinflipRules.calculateTargetWinChance(flips, minSuccesses));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public static validateActiveProgressiveClassicGame(gameActions: CoinflipClassicProgressiveGameAction[]): number {
|
|
73
|
+
if (gameActions.length === 0) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (gameActions[0].phase !== 'START') {
|
|
78
|
+
throw new Error('First action must be START');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let flipsRevealed = 0;
|
|
82
|
+
|
|
83
|
+
for (let i = 1; i < gameActions.length; i++) {
|
|
84
|
+
const action = gameActions[i];
|
|
85
|
+
switch (action.phase) {
|
|
86
|
+
case 'START':
|
|
87
|
+
throw new Error('START action can only appear as the first action');
|
|
88
|
+
case 'COIN_SELECTION':
|
|
89
|
+
if (action.selectedSide !== action.flipResult) {
|
|
90
|
+
throw new Error('COIN_SELECTION must have matching flipResult and selectedSide');
|
|
91
|
+
}
|
|
92
|
+
flipsRevealed++;
|
|
93
|
+
if (flipsRevealed > MAX_FLIPS) {
|
|
94
|
+
throw new Error(`Reconstructed flipsRevealed (${flipsRevealed}) exceeds MAX_FLIPS (${MAX_FLIPS})`);
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 'CASHOUT':
|
|
98
|
+
throw new Error('An active game cannot have a CASHOUT action');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return flipsRevealed;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
import { FixedSequenceRng } from '../fixed-rng';
|
|
3
|
+
import { CoinflipTarget } from './coinflip-target';
|
|
4
|
+
import { CoinSide, MAX_FLIPS } from './constants';
|
|
5
|
+
|
|
6
|
+
const EDGE_BPS = 200;
|
|
7
|
+
|
|
8
|
+
const getCoinRngValue = (side: CoinSide) => (side === CoinSide.HEADS ? 0 : 1);
|
|
9
|
+
|
|
10
|
+
describe('CoinflipTarget.resolve', () => {
|
|
11
|
+
it('wins when successes meet the minimum exactly', () => {
|
|
12
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS)]);
|
|
13
|
+
|
|
14
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: 3, minSuccesses: 2 }, rng, EDGE_BPS)).toMatchObject({
|
|
15
|
+
multiplier: BigNumber(1.96),
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('pays the same multiplier when successes exceed the minimum', () => {
|
|
20
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS)]);
|
|
21
|
+
|
|
22
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: 3, minSuccesses: 2 }, rng, EDGE_BPS)).toMatchObject({
|
|
23
|
+
multiplier: BigNumber(1.96),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('loses when successes fall below the minimum', () => {
|
|
28
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.TAILS)]);
|
|
29
|
+
|
|
30
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: 3, minSuccesses: 2 }, rng, EDGE_BPS)).toMatchObject({
|
|
31
|
+
multiplier: BigNumber(0),
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('counts successes for the selected side only', () => {
|
|
36
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.HEADS)]);
|
|
37
|
+
|
|
38
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.TAILS, totalFlips: 3, minSuccesses: 2 }, rng, EDGE_BPS)).toMatchObject({
|
|
39
|
+
multiplier: BigNumber(1.96),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('a single-flip target pays the same as classic', () => {
|
|
44
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS)]);
|
|
45
|
+
|
|
46
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: 1, minSuccesses: 1 }, rng, EDGE_BPS)).toMatchObject({
|
|
47
|
+
multiplier: BigNumber(1.96),
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('all correct at MAX_FLIPS pays the top multiplier', () => {
|
|
52
|
+
const rng = new FixedSequenceRng(Array(MAX_FLIPS).fill(CoinSide.HEADS).map(getCoinRngValue));
|
|
53
|
+
|
|
54
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: MAX_FLIPS, minSuccesses: MAX_FLIPS }, rng, EDGE_BPS)).toMatchObject({
|
|
55
|
+
multiplier: BigNumber(1027604.48),
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('records the flips and config in the action', () => {
|
|
60
|
+
const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.TAILS)]);
|
|
61
|
+
|
|
62
|
+
expect(CoinflipTarget.resolve({ selectedSide: CoinSide.HEADS, totalFlips: 3, minSuccesses: 2 }, rng, EDGE_BPS)).toMatchObject({
|
|
63
|
+
multiplier: BigNumber(0),
|
|
64
|
+
action: {
|
|
65
|
+
selectedSide: CoinSide.HEADS,
|
|
66
|
+
flipResults: [CoinSide.HEADS, CoinSide.TAILS, CoinSide.TAILS],
|
|
67
|
+
multiplier: BigNumber(0),
|
|
68
|
+
totalFlips: 3,
|
|
69
|
+
minSuccesses: 2,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import BigNumber from 'bignumber.js';
|
|
2
|
+
import { RandomNumberGenerator } from '../random-number-generator.interface';
|
|
3
|
+
import { CoinflipRules } from './coinflip-rules';
|
|
4
|
+
import { CoinSide } from './constants';
|
|
5
|
+
|
|
6
|
+
export interface CoinflipTargetConfig {
|
|
7
|
+
selectedSide: CoinSide;
|
|
8
|
+
totalFlips: number;
|
|
9
|
+
minSuccesses: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CoinflipTargetAction {
|
|
13
|
+
selectedSide: CoinSide;
|
|
14
|
+
flipResults: CoinSide[];
|
|
15
|
+
multiplier: BigNumber;
|
|
16
|
+
totalFlips: number;
|
|
17
|
+
minSuccesses: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CoinflipTargetGameResult {
|
|
21
|
+
multiplier: BigNumber;
|
|
22
|
+
action: CoinflipTargetAction;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class CoinflipTarget {
|
|
26
|
+
public static resolve(config: CoinflipTargetConfig, generator: RandomNumberGenerator, edge: number): CoinflipTargetGameResult {
|
|
27
|
+
const flips = CoinflipRules.getFlipResults(generator, config.totalFlips);
|
|
28
|
+
const successfulFlips = CoinflipRules.countSuccessfulFlips(flips.map(result => ({ result, selectedSide: config.selectedSide })));
|
|
29
|
+
const isLoss = successfulFlips < config.minSuccesses;
|
|
30
|
+
|
|
31
|
+
const multiplier = isLoss ? BigNumber(0) : CoinflipRules.calculateTargetMultiplier(config.totalFlips, config.minSuccesses, edge);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
multiplier,
|
|
35
|
+
action: {
|
|
36
|
+
selectedSide: config.selectedSide,
|
|
37
|
+
flipResults: flips,
|
|
38
|
+
multiplier,
|
|
39
|
+
totalFlips: config.totalFlips,
|
|
40
|
+
minSuccesses: config.minSuccesses,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { RandomNumberGenerator } from './random-number-generator.interface';
|
|
2
|
+
|
|
3
|
+
export class FixedSequenceRng implements RandomNumberGenerator {
|
|
4
|
+
constructor(private readonly values: number[]) {}
|
|
5
|
+
|
|
6
|
+
getRandomIntsWithReplacement({ count }: { min: number; max: number; count: number }): number[] {
|
|
7
|
+
const values = this.values.slice(0, count);
|
|
8
|
+
|
|
9
|
+
if (values.length !== count) {
|
|
10
|
+
throw new Error('Insufficient values provided in constructor');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return values;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getRandomIntsWithoutReplacement({ count }: { min: number; max: number; count: number }): number[] {
|
|
17
|
+
const values = this.values.slice(0, count);
|
|
18
|
+
|
|
19
|
+
if (values.length !== count) {
|
|
20
|
+
throw new Error('Insufficient values provided in constructor');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -47,3 +47,8 @@ export { formatSelectionName } from './sports/sports-name';
|
|
|
47
47
|
export { SportsContentWithFixturesInfo } from './sports/sports-content-fixture.types';
|
|
48
48
|
export { SportsProviderMaxBet, BetradarOverrideLimitType, BETRADAR_OVERRIDE_LIMIT_TO_MAX_BET } from './sports/override-limit-max-bet';
|
|
49
49
|
export { Blitz, BlitzResult, BLITZ_MIN_PICKS, BLITZ_MAX_PICKS } from './games/blitz';
|
|
50
|
+
export { CoinSide, MAX_FLIPS } from './games/coinflip/constants';
|
|
51
|
+
export * from './games/coinflip/coinflip-rules';
|
|
52
|
+
export * from './games/coinflip/coinflip-classic';
|
|
53
|
+
export * from './games/coinflip/coinflip-classic-progressive';
|
|
54
|
+
export * from './games/coinflip/coinflip-target';
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
SportsBetSelectionStatus,
|
|
8
8
|
SportsBetStatus,
|
|
9
9
|
SportsBetType,
|
|
10
|
+
SportsCashoutFailedReason,
|
|
10
11
|
SportsFixtureBannerType,
|
|
11
12
|
SportsFixtureStatus,
|
|
12
13
|
SportsMarketProductId,
|
|
@@ -132,7 +133,7 @@ export interface SportsBetSettlementV3 {
|
|
|
132
133
|
|
|
133
134
|
export interface SportsBetCashoutAvailableV3 {
|
|
134
135
|
canCashout: boolean;
|
|
135
|
-
reason:
|
|
136
|
+
reason: SportsCashoutFailedReason | null;
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
export interface SportsBetUserV3 {
|
|
@@ -85,6 +85,17 @@ export enum SportsBetStatus {
|
|
|
85
85
|
VOIDED = 'VOIDED',
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export enum SportsCashoutFailedReason {
|
|
89
|
+
SPORTS_BET_IS_CLOSED = 'SPORTS_BET_IS_CLOSED',
|
|
90
|
+
SPORTS_BET_SELECTION_NOT_FOUND = 'SPORTS_BET_SELECTION_NOT_FOUND',
|
|
91
|
+
MANUAL_MARKET_NOT_ALLOWED_CASHOUT = 'MANUAL_MARKET_NOT_ALLOWED_CASHOUT',
|
|
92
|
+
SPORTS_MARKET_CASHOUT_IS_NOT_OPEN = 'SPORTS_MARKET_CASHOUT_IS_NOT_OPEN',
|
|
93
|
+
SPORTS_MARKET_NOT_FOUND = 'SPORTS_MARKET_NOT_FOUND',
|
|
94
|
+
SPORTS_FIXTURE_NOT_FOUND = 'SPORTS_FIXTURE_NOT_FOUND',
|
|
95
|
+
SPORTS_MARKET_CLOSED = 'SPORTS_MARKET_CLOSED',
|
|
96
|
+
SPORTS_MARKET_CASHOUT_NOT_ENABLED = 'SPORTS_MARKET_CASHOUT_NOT_ENABLED',
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
export enum SportsBetType {
|
|
89
100
|
REGULAR = 'REGULAR',
|
|
90
101
|
CUSTOM_BET = 'CUSTOM_BET',
|
|
@@ -2,7 +2,8 @@ import { Chain } from "./chain";
|
|
|
2
2
|
import { Currency } from "./currency";
|
|
3
3
|
|
|
4
4
|
const CURRENCY_DISPLAY_LABEL: Partial<Record<Currency, string>> = {
|
|
5
|
-
MATIC: 'POL'
|
|
5
|
+
MATIC: 'POL',
|
|
6
|
+
TON: 'GRAM',
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
// This function is used to get the display label for a currency
|
|
@@ -11,7 +12,8 @@ export const getCurrencyLabel = (currency: Partial<Currency>): string => {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
const CHAIN_DISPLAY_LABEL: Partial<Record<Chain, string>> = {
|
|
14
|
-
MATIC_POLYGON: 'POLYGON'
|
|
15
|
+
MATIC_POLYGON: 'POLYGON',
|
|
16
|
+
TON: 'GRAM',
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const getChainLabel = (chain: Partial<Chain>): string => {
|