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.
Files changed (48) hide show
  1. package/lib/games/coinflip/coinflip-classic-progressive.d.ts +42 -0
  2. package/lib/games/coinflip/coinflip-classic-progressive.js +65 -0
  3. package/lib/games/coinflip/coinflip-classic-progressive.js.map +1 -0
  4. package/lib/games/coinflip/coinflip-classic.d.ts +18 -0
  5. package/lib/games/coinflip/coinflip-classic.js +25 -0
  6. package/lib/games/coinflip/coinflip-classic.js.map +1 -0
  7. package/lib/games/coinflip/coinflip-rules.d.ts +18 -0
  8. package/lib/games/coinflip/coinflip-rules.js +95 -0
  9. package/lib/games/coinflip/coinflip-rules.js.map +1 -0
  10. package/lib/games/coinflip/coinflip-target.d.ts +22 -0
  11. package/lib/games/coinflip/coinflip-target.js +28 -0
  12. package/lib/games/coinflip/coinflip-target.js.map +1 -0
  13. package/lib/games/coinflip/constants.d.ts +5 -0
  14. package/lib/games/coinflip/constants.js +10 -0
  15. package/lib/games/coinflip/constants.js.map +1 -0
  16. package/lib/games/fixed-rng.d.ts +15 -0
  17. package/lib/games/fixed-rng.js +24 -0
  18. package/lib/games/fixed-rng.js.map +1 -0
  19. package/lib/games/random-number-generator.interface.d.ts +12 -0
  20. package/lib/games/random-number-generator.interface.js +3 -0
  21. package/lib/games/random-number-generator.interface.js.map +1 -0
  22. package/lib/index.d.ts +5 -0
  23. package/lib/index.js +8 -1
  24. package/lib/index.js.map +1 -1
  25. package/lib/sports/sports-bet.types.d.ts +2 -2
  26. package/lib/sports/sports.types.d.ts +10 -0
  27. package/lib/sports/sports.types.js +13 -2
  28. package/lib/sports/sports.types.js.map +1 -1
  29. package/lib/tsconfig.tsbuildinfo +1 -1
  30. package/lib/utils/chain-currency-display.js +4 -2
  31. package/lib/utils/chain-currency-display.js.map +1 -1
  32. package/package.json +2 -2
  33. package/src/games/coinflip/coinflip-classic-progressive.spec.ts +251 -0
  34. package/src/games/coinflip/coinflip-classic-progressive.ts +108 -0
  35. package/src/games/coinflip/coinflip-classic.spec.ts +46 -0
  36. package/src/games/coinflip/coinflip-classic.ts +37 -0
  37. package/src/games/coinflip/coinflip-ev.spec.ts +60 -0
  38. package/src/games/coinflip/coinflip-rules.spec.ts +193 -0
  39. package/src/games/coinflip/coinflip-rules.ts +104 -0
  40. package/src/games/coinflip/coinflip-target.spec.ts +73 -0
  41. package/src/games/coinflip/coinflip-target.ts +44 -0
  42. package/src/games/coinflip/constants.ts +6 -0
  43. package/src/games/fixed-rng.ts +25 -0
  44. package/src/games/random-number-generator.interface.ts +4 -0
  45. package/src/index.ts +5 -0
  46. package/src/sports/sports-bet.types.ts +2 -1
  47. package/src/sports/sports.types.ts +11 -0
  48. package/src/utils/chain-currency-display.ts +4 -2
@@ -2,7 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getChainLabel = exports.getCurrencyLabel = void 0;
4
4
  const CURRENCY_DISPLAY_LABEL = {
5
- MATIC: 'POL'
5
+ MATIC: 'POL',
6
+ TON: 'GRAM',
6
7
  };
7
8
  const getCurrencyLabel = (currency) => {
8
9
  var _a;
@@ -10,7 +11,8 @@ const getCurrencyLabel = (currency) => {
10
11
  };
11
12
  exports.getCurrencyLabel = getCurrencyLabel;
12
13
  const CHAIN_DISPLAY_LABEL = {
13
- MATIC_POLYGON: 'POLYGON'
14
+ MATIC_POLYGON: 'POLYGON',
15
+ TON: 'GRAM',
14
16
  };
15
17
  const getChainLabel = (chain) => {
16
18
  var _a;
@@ -1 +1 @@
1
- {"version":3,"file":"chain-currency-display.js","sourceRoot":"","sources":["../../src/utils/chain-currency-display.ts"],"names":[],"mappings":";;;AAGA,MAAM,sBAAsB,GAAsC;IAChE,KAAK,EAAE,KAAK;CACb,CAAA;AAGM,MAAM,gBAAgB,GAAG,CAAC,QAA2B,EAAU,EAAE;;IACtE,OAAO,MAAA,sBAAsB,CAAC,QAAQ,CAAC,mCAAI,QAAQ,CAAC;AACtD,CAAC,CAAA;AAFY,QAAA,gBAAgB,oBAE5B;AAED,MAAM,mBAAmB,GAAmC;IAC1D,aAAa,EAAE,SAAS;CACzB,CAAA;AAEM,MAAM,aAAa,GAAG,CAAC,KAAqB,EAAU,EAAE;;IAC7D,OAAO,MAAA,mBAAmB,CAAC,KAAK,CAAC,mCAAI,KAAK,CAAC;AAC7C,CAAC,CAAA;AAFY,QAAA,aAAa,iBAEzB"}
1
+ {"version":3,"file":"chain-currency-display.js","sourceRoot":"","sources":["../../src/utils/chain-currency-display.ts"],"names":[],"mappings":";;;AAGA,MAAM,sBAAsB,GAAsC;IAChE,KAAK,EAAE,KAAK;IACZ,GAAG,EAAE,MAAM;CACZ,CAAA;AAGM,MAAM,gBAAgB,GAAG,CAAC,QAA2B,EAAU,EAAE;;IACtE,OAAO,MAAA,sBAAsB,CAAC,QAAQ,CAAC,mCAAI,QAAQ,CAAC;AACtD,CAAC,CAAA;AAFY,QAAA,gBAAgB,oBAE5B;AAED,MAAM,mBAAmB,GAAmC;IAC1D,aAAa,EAAE,SAAS;IACxB,GAAG,EAAE,MAAM;CACZ,CAAA;AAEM,MAAM,aAAa,GAAG,CAAC,KAAqB,EAAU,EAAE;;IAC7D,OAAO,MAAA,mBAAmB,CAAC,KAAK,CAAC,mCAAI,KAAK,CAAC;AAC7C,CAAC,CAAA;AAFY,QAAA,aAAa,iBAEzB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "3.3.12",
3
+ "version": "3.3.14",
4
4
  "description": "",
5
5
  "types": "lib/index.d.ts",
6
6
  "main": "lib/index.js",
@@ -14,5 +14,5 @@
14
14
  },
15
15
  "author": "",
16
16
  "license": "ISC",
17
- "gitHead": "72c940719346f25366185d52c9742917410bffa7"
17
+ "gitHead": "1ee974fa20540765a0bed2a70ca07123b065937d"
18
18
  }
@@ -0,0 +1,251 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { FixedSequenceRng } from '../fixed-rng';
3
+ import {
4
+ CoinflipClassicProgressive,
5
+ CoinflipProgressiveCashoutAction,
6
+ CoinflipProgressiveCoinSelectionAction,
7
+ CoinflipProgressiveStartAction,
8
+ } from './coinflip-classic-progressive';
9
+ import { CoinSide, MAX_FLIPS } from './constants';
10
+
11
+ const EDGE_BPS = 200;
12
+
13
+ const getCoinRngValue = (side: CoinSide) => (side === CoinSide.HEADS ? 0 : 1);
14
+
15
+ const startAction = (): CoinflipProgressiveStartAction => ({ phase: 'START' });
16
+ const winAction = (side: CoinSide): CoinflipProgressiveCoinSelectionAction => ({
17
+ phase: 'COIN_SELECTION',
18
+ selectedSide: side,
19
+ flipResult: side,
20
+ multiplier: new BigNumber(0),
21
+ });
22
+ const cashoutAction = (multiplier = 1.96): CoinflipProgressiveCashoutAction => ({
23
+ phase: 'CASHOUT',
24
+ multiplier: new BigNumber(multiplier),
25
+ });
26
+
27
+ const gameWithWins = (wins: number) =>
28
+ CoinflipClassicProgressive.fromActions([startAction(), ...Array.from({ length: wins }, () => winAction(CoinSide.HEADS))]);
29
+
30
+ describe('CoinflipClassicProgressive.createAction', () => {
31
+ it('returns a START action', () => {
32
+ expect(CoinflipClassicProgressive.createAction()).toEqual({ phase: 'START' });
33
+ });
34
+ });
35
+
36
+ describe('CoinflipClassicProgressive.fromActions', () => {
37
+ describe('empty history', () => {
38
+ it('returns a game with flipsRevealed = 0', () => {
39
+ const game = CoinflipClassicProgressive.fromActions([]);
40
+ expect(game.canCashout()).toBe(false);
41
+ });
42
+ });
43
+
44
+ describe('valid histories', () => {
45
+ it('a start-only history cannot be cashed out', () => {
46
+ expect(CoinflipClassicProgressive.fromActions([startAction()]).canCashout()).toBe(false);
47
+ });
48
+
49
+ it('accumulates winning actions into cashout eligibility', () => {
50
+ expect(gameWithWins(3).canCashout()).toBe(true);
51
+ });
52
+
53
+ it('accepts flips won on different sides', () => {
54
+ const game = CoinflipClassicProgressive.fromActions([startAction(), winAction(CoinSide.HEADS), winAction(CoinSide.TAILS)]);
55
+ expect(game.canCashout()).toBe(true);
56
+ });
57
+
58
+ it('throws when reconstructed from an already cashed-out history', () => {
59
+ expect(() =>
60
+ CoinflipClassicProgressive.fromActions([startAction(), winAction(CoinSide.HEADS), winAction(CoinSide.HEADS), cashoutAction(3.92)]),
61
+ ).toThrow('An active game cannot have a CASHOUT action');
62
+ });
63
+ });
64
+
65
+ describe('validation errors', () => {
66
+ it('throws when the first action is not START', () => {
67
+ expect(() => CoinflipClassicProgressive.fromActions([winAction(CoinSide.HEADS)])).toThrow('First action must be START');
68
+ });
69
+
70
+ it('throws when a START action appears after the first position', () => {
71
+ expect(() => CoinflipClassicProgressive.fromActions([startAction(), winAction(CoinSide.HEADS), startAction()])).toThrow(
72
+ 'START action can only appear as the first action',
73
+ );
74
+ });
75
+
76
+ it('throws when a COIN_SELECTION has mismatched flipResult', () => {
77
+ const badWinAction: CoinflipProgressiveCoinSelectionAction = {
78
+ phase: 'COIN_SELECTION',
79
+ selectedSide: CoinSide.HEADS,
80
+ flipResult: CoinSide.TAILS,
81
+ multiplier: new BigNumber(0),
82
+ };
83
+ expect(() => CoinflipClassicProgressive.fromActions([startAction(), badWinAction])).toThrow(
84
+ 'COIN_SELECTION must have matching flipResult and selectedSide',
85
+ );
86
+ });
87
+
88
+ it('throws when CASHOUT appears before any wins', () => {
89
+ expect(() => CoinflipClassicProgressive.fromActions([startAction(), cashoutAction()])).toThrow('An active game cannot have a CASHOUT action');
90
+ });
91
+
92
+ it('throws when the history contains more than MAX_FLIPS flips', () => {
93
+ const tooManyWins = Array.from({ length: MAX_FLIPS + 1 }, () => winAction(CoinSide.HEADS));
94
+ expect(() => CoinflipClassicProgressive.fromActions([startAction(), ...tooManyWins])).toThrow(
95
+ `Reconstructed flipsRevealed (${MAX_FLIPS + 1}) exceeds MAX_FLIPS (${MAX_FLIPS})`,
96
+ );
97
+ });
98
+ });
99
+ });
100
+
101
+ describe('CoinflipClassicProgressive.next', () => {
102
+ it('a winning first flip pays 1.96x and keeps the game open', () => {
103
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS)]);
104
+ expect(gameWithWins(0).next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
105
+ multiplier: new BigNumber(1.96),
106
+ isWin: true,
107
+ isComplete: false,
108
+ gameAction: {
109
+ phase: 'COIN_SELECTION',
110
+ selectedSide: CoinSide.HEADS,
111
+ flipResult: CoinSide.HEADS,
112
+ multiplier: new BigNumber(1.96),
113
+ },
114
+ });
115
+ });
116
+
117
+ it('a losing flip zeroes the multiplier and completes the game', () => {
118
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.TAILS)]);
119
+ expect(gameWithWins(0).next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
120
+ multiplier: new BigNumber(0),
121
+ isWin: false,
122
+ isComplete: true,
123
+ gameAction: {
124
+ phase: 'COIN_SELECTION',
125
+ selectedSide: CoinSide.HEADS,
126
+ flipResult: CoinSide.TAILS,
127
+ multiplier: new BigNumber(0),
128
+ },
129
+ });
130
+ });
131
+
132
+ it('a loss deep into a streak still pays zero', () => {
133
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS)]);
134
+ expect(gameWithWins(5).next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
135
+ multiplier: new BigNumber(0),
136
+ isWin: false,
137
+ isComplete: true,
138
+ gameAction: { flipResult: CoinSide.TAILS },
139
+ });
140
+ });
141
+
142
+ it('reveals the last drawn value as the new flip', () => {
143
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.HEADS)]);
144
+ expect(gameWithWins(2).next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
145
+ multiplier: new BigNumber(7.84),
146
+ gameAction: { flipResult: CoinSide.HEADS },
147
+ });
148
+ });
149
+
150
+ it('allows switching sides between flips', () => {
151
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS)]);
152
+ const game = CoinflipClassicProgressive.fromActions([startAction(), winAction(CoinSide.HEADS)]);
153
+ expect(game.next(CoinSide.TAILS, rng, EDGE_BPS)).toMatchObject({
154
+ multiplier: new BigNumber(3.92),
155
+ isWin: true,
156
+ gameAction: { selectedSide: CoinSide.TAILS, flipResult: CoinSide.TAILS },
157
+ });
158
+ });
159
+
160
+ it('a win on the final flip completes the game at the top multiplier', () => {
161
+ const rng = new FixedSequenceRng(Array(MAX_FLIPS).fill(CoinSide.HEADS).map(getCoinRngValue));
162
+ expect(gameWithWins(MAX_FLIPS - 1).next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
163
+ multiplier: new BigNumber(1027604.48),
164
+ isWin: true,
165
+ isComplete: true,
166
+ });
167
+ });
168
+
169
+ it('works from an empty history (no start action persisted)', () => {
170
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS)]);
171
+ const game = CoinflipClassicProgressive.fromActions([]);
172
+ expect(game.next(CoinSide.HEADS, rng, EDGE_BPS)).toMatchObject({
173
+ multiplier: new BigNumber(1.96),
174
+ isWin: true,
175
+ isComplete: false,
176
+ gameAction: { phase: 'COIN_SELECTION', selectedSide: CoinSide.HEADS, flipResult: CoinSide.HEADS },
177
+ });
178
+ });
179
+
180
+ it('throws when MAX_FLIPS are already revealed', () => {
181
+ const rng = new FixedSequenceRng(Array(MAX_FLIPS + 1).fill(CoinSide.HEADS).map(getCoinRngValue));
182
+ expect(() => gameWithWins(MAX_FLIPS).next(CoinSide.HEADS, rng, EDGE_BPS)).toThrow();
183
+ });
184
+ });
185
+
186
+ describe('CoinflipClassicProgressive.cashout', () => {
187
+ it.each([
188
+ [1, 1.96],
189
+ [2, 3.92],
190
+ [3, 7.84],
191
+ [MAX_FLIPS, 1027604.48],
192
+ ])('cashing out after %i wins pays %fx', (wins, expected) => {
193
+ expect(gameWithWins(wins).cashout(EDGE_BPS)).toMatchObject({
194
+ multiplier: new BigNumber(expected),
195
+ gameAction: {
196
+ phase: 'CASHOUT',
197
+ multiplier: new BigNumber(expected),
198
+ },
199
+ });
200
+ });
201
+
202
+ it('throws with zero revealed flips', () => {
203
+ expect(() => gameWithWins(0).cashout(EDGE_BPS)).toThrow();
204
+ });
205
+ });
206
+
207
+ describe('CoinflipClassicProgressive.autobet', () => {
208
+ it('wins and pays the N-flip classic multiplier when all sides match', () => {
209
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS)]);
210
+ expect(CoinflipClassicProgressive.autobet([CoinSide.HEADS, CoinSide.HEADS, CoinSide.TAILS], rng, EDGE_BPS)).toMatchObject({
211
+ multiplier: new BigNumber(7.84),
212
+ selectedSides: [CoinSide.HEADS, CoinSide.HEADS, CoinSide.TAILS],
213
+ flipResults: [CoinSide.HEADS, CoinSide.HEADS, CoinSide.TAILS],
214
+ });
215
+ });
216
+
217
+ it('loses with zero multiplier when a flip mismatches', () => {
218
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.HEADS)]);
219
+ expect(CoinflipClassicProgressive.autobet([CoinSide.HEADS, CoinSide.HEADS, CoinSide.HEADS], rng, EDGE_BPS)).toMatchObject({
220
+ multiplier: new BigNumber(0),
221
+ });
222
+ });
223
+
224
+ it('pads flipResults to full length even after an early loss', () => {
225
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.HEADS)]);
226
+ const { flipResults } = CoinflipClassicProgressive.autobet([CoinSide.HEADS, CoinSide.HEADS, CoinSide.HEADS], rng, EDGE_BPS);
227
+ expect(flipResults).toHaveLength(3);
228
+ expect(flipResults).toEqual([CoinSide.HEADS, CoinSide.TAILS, CoinSide.HEADS]);
229
+ });
230
+
231
+ it('uses the same provably-fair RNG scheme as manual next() — flip i uses value at index i', () => {
232
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS), getCoinRngValue(CoinSide.TAILS), getCoinRngValue(CoinSide.HEADS)]);
233
+ const { flipResults } = CoinflipClassicProgressive.autobet([CoinSide.HEADS, CoinSide.TAILS, CoinSide.HEADS], rng, EDGE_BPS);
234
+ expect(flipResults).toEqual([CoinSide.HEADS, CoinSide.TAILS, CoinSide.HEADS]);
235
+ });
236
+
237
+ it('a single-flip autobet matches classic payout', () => {
238
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS)]);
239
+ expect(CoinflipClassicProgressive.autobet([CoinSide.HEADS], rng, EDGE_BPS)).toMatchObject({
240
+ multiplier: new BigNumber(1.96),
241
+ flipResults: [CoinSide.HEADS],
242
+ });
243
+ });
244
+
245
+ it('all MAX_FLIPS correct pays the top multiplier', () => {
246
+ const rng = new FixedSequenceRng(Array(MAX_FLIPS).fill(CoinSide.HEADS).map(getCoinRngValue));
247
+ const { multiplier, flipResults } = CoinflipClassicProgressive.autobet(Array(MAX_FLIPS).fill(CoinSide.HEADS), rng, EDGE_BPS);
248
+ expect(multiplier).toEqual(new BigNumber(1027604.48));
249
+ expect(flipResults).toHaveLength(MAX_FLIPS);
250
+ });
251
+ });
@@ -0,0 +1,108 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { RandomNumberGenerator } from '../random-number-generator.interface';
3
+ import { CoinflipRules } from './coinflip-rules';
4
+ import { CoinSide, MAX_FLIPS } from './constants';
5
+
6
+ export interface CoinflipProgressiveStartAction {
7
+ phase: 'START';
8
+ }
9
+
10
+ export interface CoinflipProgressiveCoinSelectionAction {
11
+ phase: 'COIN_SELECTION';
12
+ selectedSide: CoinSide;
13
+ flipResult: CoinSide;
14
+ multiplier: BigNumber;
15
+ }
16
+
17
+ export interface CoinflipProgressiveCashoutAction {
18
+ phase: 'CASHOUT';
19
+ multiplier: BigNumber;
20
+ }
21
+
22
+ export type CoinflipClassicProgressiveGameAction =
23
+ | CoinflipProgressiveStartAction
24
+ | CoinflipProgressiveCoinSelectionAction
25
+ | CoinflipProgressiveCashoutAction;
26
+
27
+ export interface CoinflipClassicProgressiveNextResult {
28
+ multiplier: BigNumber;
29
+ gameAction: CoinflipProgressiveCoinSelectionAction;
30
+ isWin: boolean;
31
+ isComplete: boolean;
32
+ }
33
+
34
+ export interface CoinflipClassicProgressiveGameResult {
35
+ multiplier: BigNumber;
36
+ gameAction: CoinflipProgressiveCashoutAction;
37
+ }
38
+
39
+ export interface CoinflipClassicProgressiveAutoBetResult {
40
+ multiplier: BigNumber;
41
+ selectedSides: CoinSide[];
42
+ flipResults: CoinSide[];
43
+ }
44
+
45
+ export class CoinflipClassicProgressive {
46
+ private constructor(private readonly flipsRevealed: number) {}
47
+
48
+ public static createAction(): CoinflipProgressiveStartAction {
49
+ return { phase: 'START' };
50
+ }
51
+
52
+ public static fromActions(gameActions: CoinflipClassicProgressiveGameAction[]): CoinflipClassicProgressive {
53
+ if (gameActions.length === 0) {
54
+ return new CoinflipClassicProgressive(0);
55
+ }
56
+
57
+ const flipsRevealed = CoinflipRules.validateActiveProgressiveClassicGame(gameActions);
58
+ return new CoinflipClassicProgressive(flipsRevealed);
59
+ }
60
+
61
+ public canCashout(): boolean {
62
+ return this.flipsRevealed > 0;
63
+ }
64
+
65
+ public next(selectedSide: CoinSide, generator: RandomNumberGenerator, edge: number): CoinflipClassicProgressiveNextResult {
66
+ const newFlip = CoinflipRules.getFlipResultAtPosition(generator, this.flipsRevealed + 1);
67
+ const isWin = CoinflipRules.isFlipWin(selectedSide, newFlip);
68
+
69
+ const revealedAfter = isWin ? this.flipsRevealed + 1 : this.flipsRevealed;
70
+ const isComplete = !isWin || revealedAfter >= MAX_FLIPS;
71
+ const multiplier = isWin ? CoinflipRules.calculateClassicMultiplier(revealedAfter, edge) : BigNumber(0);
72
+
73
+ return {
74
+ multiplier,
75
+ gameAction: {
76
+ phase: 'COIN_SELECTION',
77
+ selectedSide,
78
+ flipResult: newFlip,
79
+ multiplier,
80
+ },
81
+ isWin,
82
+ isComplete,
83
+ };
84
+ }
85
+
86
+ public static autobet(selectedSides: CoinSide[], generator: RandomNumberGenerator, edge: number): CoinflipClassicProgressiveAutoBetResult {
87
+ const pairs = selectedSides.map((selectedSide, i) => {
88
+ return { selectedSide, result: CoinflipRules.getFlipResultAtPosition(generator, i + 1) };
89
+ });
90
+
91
+ const won = CoinflipRules.countSuccessfulFlips(pairs) === selectedSides.length;
92
+ const multiplier = won ? CoinflipRules.calculateClassicMultiplier(selectedSides.length, edge) : BigNumber(0);
93
+ return { multiplier, selectedSides, flipResults: pairs.map(p => p.result) };
94
+ }
95
+
96
+ public cashout(edge: number): CoinflipClassicProgressiveGameResult {
97
+ if (!this.canCashout()) {
98
+ throw new Error('cashout called with no flips revealed');
99
+ }
100
+
101
+ const multiplier = CoinflipRules.calculateClassicMultiplier(this.flipsRevealed, edge);
102
+
103
+ return {
104
+ multiplier,
105
+ gameAction: { phase: 'CASHOUT', multiplier },
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,46 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { FixedSequenceRng } from '../fixed-rng';
3
+ import { CoinflipClassic } from './coinflip-classic';
4
+ import { CoinSide } from './constants';
5
+
6
+ const EDGE_BPS = 200;
7
+
8
+ const getCoinRngValue = (side: CoinSide) => (side === CoinSide.HEADS ? 0 : 1);
9
+
10
+ describe('CoinflipClassic.resolve', () => {
11
+ it('matching flip wins 1.96x', () => {
12
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.HEADS)]);
13
+ expect(CoinflipClassic.resolve({ selectedSide: CoinSide.HEADS }, rng, EDGE_BPS)).toMatchObject({
14
+ multiplier: new BigNumber(1.96),
15
+ action: {
16
+ selectedSide: CoinSide.HEADS,
17
+ flipResult: CoinSide.HEADS,
18
+ multiplier: new BigNumber(1.96),
19
+ },
20
+ });
21
+ });
22
+
23
+ it('wins on tails when tails is selected', () => {
24
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.TAILS)]);
25
+ expect(CoinflipClassic.resolve({ selectedSide: CoinSide.TAILS }, rng, EDGE_BPS)).toMatchObject({
26
+ multiplier: new BigNumber(1.96),
27
+ action: {
28
+ selectedSide: CoinSide.TAILS,
29
+ flipResult: CoinSide.TAILS,
30
+ multiplier: new BigNumber(1.96),
31
+ },
32
+ });
33
+ });
34
+
35
+ it('mismatched flip loses with zero multiplier', () => {
36
+ const rng = new FixedSequenceRng([getCoinRngValue(CoinSide.TAILS)]);
37
+ expect(CoinflipClassic.resolve({ selectedSide: CoinSide.HEADS }, rng, EDGE_BPS)).toMatchObject({
38
+ multiplier: new BigNumber(0),
39
+ action: {
40
+ selectedSide: CoinSide.HEADS,
41
+ flipResult: CoinSide.TAILS,
42
+ multiplier: new BigNumber(0),
43
+ },
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,37 @@
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 CoinflipClassicConfig {
7
+ selectedSide: CoinSide;
8
+ }
9
+
10
+ export interface CoinflipClassicAction {
11
+ selectedSide: CoinSide;
12
+ flipResult: CoinSide;
13
+ multiplier: BigNumber;
14
+ }
15
+
16
+ export interface CoinflipClassicGameResult {
17
+ multiplier: BigNumber;
18
+ action: CoinflipClassicAction;
19
+ }
20
+
21
+ export class CoinflipClassic {
22
+ public static resolve(config: CoinflipClassicConfig, generator: RandomNumberGenerator, edge: number): CoinflipClassicGameResult {
23
+ const flip = CoinflipRules.getFlipResultAtPosition(generator, 1);
24
+ const isWin = CoinflipRules.isFlipWin(config.selectedSide, flip);
25
+
26
+ const multiplier = isWin ? CoinflipRules.calculateClassicMultiplier(1, edge) : BigNumber(0);
27
+
28
+ return {
29
+ multiplier,
30
+ action: {
31
+ selectedSide: config.selectedSide,
32
+ flipResult: flip,
33
+ multiplier,
34
+ },
35
+ };
36
+ }
37
+ }
@@ -0,0 +1,60 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { CoinflipRules } from './coinflip-rules';
3
+ import { MAX_FLIPS } from './constants';
4
+
5
+ const EDGE_BPS = 200;
6
+ const EXPECTED_EV = 1 - EDGE_BPS / 10000;
7
+
8
+ // successCounts[n][k] = number of n-flip outcomes with exactly k successes (row n of Pascal's triangle).
9
+ // Built once by brute-force enumeration, independent of the nCr formula under test.
10
+ function buildSuccessCounts(maxFlips: number): number[][] {
11
+ const counts: number[][] = [];
12
+ for (let n = 0; n <= maxFlips; n++) {
13
+ const histogram = new Array<number>(n + 1).fill(0);
14
+ const total = 2 ** n;
15
+ for (let outcome = 0; outcome < total; outcome++) {
16
+ const successes = outcome
17
+ .toString(2)
18
+ .split('')
19
+ .filter(bit => bit === '1').length;
20
+ histogram[successes]++;
21
+ }
22
+ counts[n] = histogram;
23
+ }
24
+ return counts;
25
+ }
26
+
27
+ const SUCCESS_COUNTS = buildSuccessCounts(MAX_FLIPS);
28
+
29
+ describe('CoinflipRules.calculateClassicMultiplier empirical EV', () => {
30
+ for (let flips = 1; flips <= MAX_FLIPS; flips++) {
31
+ it(`n=${flips}: EV across all 2^${flips} outcomes equals 1 - edge`, () => {
32
+ const total = 2 ** flips;
33
+ const wins = SUCCESS_COUNTS[flips][flips];
34
+
35
+ const multiplier = CoinflipRules.calculateClassicMultiplier(flips, EDGE_BPS);
36
+ const ev = BigNumber(wins).dividedBy(total).multipliedBy(multiplier);
37
+
38
+ expect(ev.toNumber()).toBeCloseTo(EXPECTED_EV, 10);
39
+ });
40
+ }
41
+ });
42
+
43
+ describe('CoinflipRules.calculateTargetMultiplier empirical EV', () => {
44
+ for (let flips = 1; flips <= MAX_FLIPS; flips++) {
45
+ for (let minSuccesses = 1; minSuccesses <= flips; minSuccesses++) {
46
+ it(`n=${flips} minSuccesses=${minSuccesses}: EV equals 1 - edge`, () => {
47
+ const total = 2 ** flips;
48
+ let wins = 0;
49
+ for (let k = minSuccesses; k <= flips; k++) {
50
+ wins += SUCCESS_COUNTS[flips][k];
51
+ }
52
+
53
+ const multiplier = CoinflipRules.calculateTargetMultiplier(flips, minSuccesses, EDGE_BPS);
54
+ const ev = BigNumber(wins).dividedBy(total).multipliedBy(multiplier);
55
+
56
+ expect(ev.toNumber()).toBeCloseTo(EXPECTED_EV, 10);
57
+ });
58
+ }
59
+ }
60
+ });