shufflecom-calculations 4.2.4 → 4.3.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.
Files changed (45) hide show
  1. package/lib/games/baccarat.d.ts +1 -0
  2. package/lib/games/baccarat.js +19 -0
  3. package/lib/games/baccarat.js.map +1 -1
  4. package/lib/games/blackjack.d.ts +16 -0
  5. package/lib/games/blackjack.js +34 -4
  6. package/lib/games/blackjack.js.map +1 -1
  7. package/lib/games/coinflip/coinflip-classic-progressive.d.ts +1 -0
  8. package/lib/games/coinflip/coinflip-classic-progressive.js +4 -0
  9. package/lib/games/coinflip/coinflip-classic-progressive.js.map +1 -1
  10. package/lib/games/hilo.d.ts +6 -0
  11. package/lib/games/hilo.js +25 -1
  12. package/lib/games/hilo.js.map +1 -1
  13. package/lib/games/keno.d.ts +1 -0
  14. package/lib/games/keno.js +7 -0
  15. package/lib/games/keno.js.map +1 -1
  16. package/lib/games/plinko.d.ts +1 -0
  17. package/lib/games/plinko.js +7 -0
  18. package/lib/games/plinko.js.map +1 -1
  19. package/lib/games/roulette.d.ts +1 -0
  20. package/lib/games/roulette.js +3 -0
  21. package/lib/games/roulette.js.map +1 -1
  22. package/lib/games/wheel.d.ts +1 -0
  23. package/lib/games/wheel.js +3 -0
  24. package/lib/games/wheel.js.map +1 -1
  25. package/lib/sports/sports-competition-fixture.types.d.ts +1 -0
  26. package/lib/sports/sports-competition-fixture.types.js.map +1 -1
  27. package/lib/tsconfig.tsbuildinfo +1 -1
  28. package/package.json +2 -2
  29. package/src/games/baccarat.spec.ts +60 -5
  30. package/src/games/baccarat.ts +21 -1
  31. package/src/games/blackjack.spec.ts +100 -0
  32. package/src/games/blackjack.ts +78 -8
  33. package/src/games/coinflip/coinflip-classic-progressive.spec.ts +9 -0
  34. package/src/games/coinflip/coinflip-classic-progressive.ts +5 -0
  35. package/src/games/hilo.spec.ts +54 -1
  36. package/src/games/hilo.ts +35 -1
  37. package/src/games/keno.spec.ts +20 -1
  38. package/src/games/keno.ts +8 -0
  39. package/src/games/plinko.spec.ts +19 -0
  40. package/src/games/plinko.ts +8 -0
  41. package/src/games/roulette.spec.ts +22 -2
  42. package/src/games/roulette.ts +4 -0
  43. package/src/games/wheel.spec.ts +9 -0
  44. package/src/games/wheel.ts +4 -0
  45. package/src/sports/sports-competition-fixture.types.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "4.2.4",
3
+ "version": "4.3.0",
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": "8ccea4c69581b221ba1ab1c2bcb4c465f2c53254"
17
+ "gitHead": "6ab5874d4c58fdc438fa41165b12626a72143af6"
18
18
  }
@@ -887,7 +887,13 @@ describe('Baccarat', () => {
887
887
  const card = ci('4', Suits.CLUBS);
888
888
  const result = mockResult([card, card], [ci('3', Suits.CLUBS), ci('2', Suits.DIAMONDS)], 8, 5, BaccaratOutcome.PLAYER_WIN);
889
889
  expect(Baccarat.getWinningBetTypes(result)).toEqual(
890
- expect.arrayContaining([BaccaratBetType.PLAYER, BaccaratBetType.PLAYER_PAIR, BaccaratBetType.EITHER_PAIR, BaccaratBetType.PERFECT_PAIR, BaccaratBetType.PLAYER_BONUS]),
890
+ expect.arrayContaining([
891
+ BaccaratBetType.PLAYER,
892
+ BaccaratBetType.PLAYER_PAIR,
893
+ BaccaratBetType.EITHER_PAIR,
894
+ BaccaratBetType.PERFECT_PAIR,
895
+ BaccaratBetType.PLAYER_BONUS,
896
+ ]),
891
897
  );
892
898
  });
893
899
 
@@ -895,7 +901,13 @@ describe('Baccarat', () => {
895
901
  const card = ci('K', Suits.DIAMONDS);
896
902
  const result = mockResult([ci('3', Suits.CLUBS), ci('2', Suits.DIAMONDS)], [card, card], 5, 0, BaccaratOutcome.PLAYER_WIN);
897
903
  expect(Baccarat.getWinningBetTypes(result)).toEqual(
898
- expect.arrayContaining([BaccaratBetType.PLAYER, BaccaratBetType.BANKER_PAIR, BaccaratBetType.EITHER_PAIR, BaccaratBetType.PERFECT_PAIR, BaccaratBetType.PLAYER_BONUS]),
904
+ expect.arrayContaining([
905
+ BaccaratBetType.PLAYER,
906
+ BaccaratBetType.BANKER_PAIR,
907
+ BaccaratBetType.EITHER_PAIR,
908
+ BaccaratBetType.PERFECT_PAIR,
909
+ BaccaratBetType.PLAYER_BONUS,
910
+ ]),
899
911
  );
900
912
  });
901
913
 
@@ -1001,9 +1013,7 @@ describe('Baccarat', () => {
1001
1013
  8,
1002
1014
  BaccaratOutcome.TIE,
1003
1015
  );
1004
- expect(Baccarat.getWinningBetTypes(result)).toEqual(
1005
- expect.arrayContaining([BaccaratBetType.TIE]),
1006
- );
1016
+ expect(Baccarat.getWinningBetTypes(result)).toEqual(expect.arrayContaining([BaccaratBetType.TIE]));
1007
1017
  });
1008
1018
 
1009
1019
  it('returns neither bonus on non-natural tie', () => {
@@ -1028,4 +1038,49 @@ describe('Baccarat', () => {
1028
1038
  expect(multiplier.toNumber()).toBe(1.95);
1029
1039
  });
1030
1040
  });
1041
+
1042
+ describe('getMaxMultiplier', () => {
1043
+ it.each<[BaccaratBetType, number]>([
1044
+ [BaccaratBetType.PLAYER, 2], // 1:1
1045
+ [BaccaratBetType.BANKER, 1.95], // 0.95:1 (commission)
1046
+ [BaccaratBetType.TIE, 9], // 8:1
1047
+ [BaccaratBetType.PLAYER_PAIR, 12], // 11:1
1048
+ [BaccaratBetType.BANKER_PAIR, 12], // 11:1
1049
+ [BaccaratBetType.PERFECT_PAIR, 26], // 25:1 (one hand)
1050
+ [BaccaratBetType.EITHER_PAIR, 6], // 5:1
1051
+ [BaccaratBetType.PLAYER_BONUS, 31], // 30:1 (non-natural win by 9)
1052
+ [BaccaratBetType.BANKER_BONUS, 31], // 30:1 (non-natural win by 9)
1053
+ ])('a single %s bet caps at its own max multiplier (%f)', (type, expected) => {
1054
+ expect(Baccarat.getMaxMultiplier([bet(type, 10)]).toNumber()).toBe(expected);
1055
+ });
1056
+
1057
+ it.each([
1058
+ { label: 'stake-heavy on PLAYER', bets: [bet(BaccaratBetType.PLAYER, 30), bet(BaccaratBetType.TIE, 10)], expected: 3.75 },
1059
+ { label: 'stake-heavy on TIE', bets: [bet(BaccaratBetType.PLAYER, 10), bet(BaccaratBetType.TIE, 30)], expected: 7.25 },
1060
+ ])('blends per-type maxes weighted by stake ($label)', ({ bets, expected }) => {
1061
+ expect(Baccarat.getMaxMultiplier(bets).toNumber()).toBe(expected);
1062
+ });
1063
+
1064
+ it('sums per-type maxes across a full side-bet spread', () => {
1065
+ const bets = [
1066
+ bet(BaccaratBetType.PLAYER, 10),
1067
+ bet(BaccaratBetType.PERFECT_PAIR, 10),
1068
+ bet(BaccaratBetType.BANKER_PAIR, 10),
1069
+ bet(BaccaratBetType.PLAYER_BONUS, 10),
1070
+ ];
1071
+ expect(Baccarat.getMaxMultiplier(bets).toNumber()).toBe(17.75);
1072
+ });
1073
+
1074
+ it('accumulates repeated bets of the same type', () => {
1075
+ const bets = [bet(BaccaratBetType.BANKER, 25), bet(BaccaratBetType.BANKER, 75)];
1076
+ expect(Baccarat.getMaxMultiplier(bets).toNumber()).toBe(1.95);
1077
+ });
1078
+
1079
+ it.each([
1080
+ { label: 'no bets', bets: [] as ReturnType<typeof bet>[] },
1081
+ { label: 'only zero-amount bets', bets: [bet(BaccaratBetType.PLAYER, 0), bet(BaccaratBetType.TIE, 0)] },
1082
+ ])('returns 0 when the total stake is 0 ($label)', ({ bets }) => {
1083
+ expect(Baccarat.getMaxMultiplier(bets).toNumber()).toBe(0);
1084
+ });
1085
+ });
1031
1086
  });
@@ -57,6 +57,26 @@ const BONUS_NON_NATURAL_PAYOUT: Record<number, BigNumber> = {
57
57
  };
58
58
 
59
59
  export class Baccarat {
60
+ static getMaxMultiplier(bets: BaccaratBet[]): BigNumber {
61
+ const MAX_BY_TYPE: Record<BaccaratBetType, BigNumber> = {
62
+ [BaccaratBetType.PLAYER]: PLAYER_WIN_MULTIPLIER,
63
+ [BaccaratBetType.BANKER]: BANKER_WIN_MULTIPLIER,
64
+ [BaccaratBetType.TIE]: TIE_MULTIPLIER,
65
+ [BaccaratBetType.PLAYER_PAIR]: PAIR_MULTIPLIER,
66
+ [BaccaratBetType.BANKER_PAIR]: PAIR_MULTIPLIER,
67
+ [BaccaratBetType.PERFECT_PAIR]: PERFECT_PAIR_ONE_HAND_MULTIPLIER,
68
+ [BaccaratBetType.EITHER_PAIR]: EITHER_PAIR_MULTIPLIER,
69
+ [BaccaratBetType.PLAYER_BONUS]: new BigNumber(31),
70
+ [BaccaratBetType.BANKER_BONUS]: new BigNumber(31),
71
+ };
72
+ const totalBet = bets.reduce((sum, bet) => sum.plus(bet.amount), new BigNumber(0));
73
+ if (totalBet.isZero()) {
74
+ return new BigNumber(0);
75
+ }
76
+ const maxPayout = bets.reduce((sum, bet) => sum.plus(bet.amount.multipliedBy(MAX_BY_TYPE[bet.type])), new BigNumber(0));
77
+ return maxPayout.dividedBy(totalBet);
78
+ }
79
+
60
80
  static getBaccaratCardValue(cardIndex: number): number {
61
81
  const { value } = CardGames.getCardDetails(cardIndex);
62
82
  if (value === '10' || value === 'J' || value === 'Q' || value === 'K') {
@@ -224,7 +244,7 @@ export class Baccarat {
224
244
  if (outcome === BaccaratOutcome.PLAYER_WIN && (playerIsNatural || (!playerIsNatural && playerHandValue - bankerHandValue >= 4))) {
225
245
  winning.push(BaccaratBetType.PLAYER_BONUS);
226
246
  }
227
- if (outcome === BaccaratOutcome.BANKER_WIN && (bankerIsNatural || (!bankerIsNatural && bankerHandValue - playerHandValue >= 4))) {
247
+ if (outcome === BaccaratOutcome.BANKER_WIN && (bankerIsNatural || (!bankerIsNatural && bankerHandValue - playerHandValue >= 4))) {
228
248
  winning.push(BaccaratBetType.BANKER_BONUS);
229
249
  }
230
250
 
@@ -1521,3 +1521,103 @@ describe('Blackjack', () => {
1521
1521
  */
1522
1522
  });
1523
1523
  });
1524
+
1525
+ describe('Blackjack.getMaxPotentialPayout', () => {
1526
+ it('is the natural 2.5x main bet plus sidebet ceilings (26x perfect pair, 101x 21+3)', () => {
1527
+ const payout = Blackjack.getMaxPotentialPayout({
1528
+ mainBetAmount: new BigNumber(10),
1529
+ perfectPairAmount: new BigNumber(2),
1530
+ twentyOnePlusThreeAmount: new BigNumber(3),
1531
+ });
1532
+ // 2.5*10 + 26*2 + 101*3 = 25 + 52 + 303 = 380
1533
+ expect(payout.toNumber()).toBe(380);
1534
+ });
1535
+
1536
+ it('is 2.5x main bet when there are no sidebets', () => {
1537
+ const payout = Blackjack.getMaxPotentialPayout({
1538
+ mainBetAmount: new BigNumber(100),
1539
+ perfectPairAmount: new BigNumber(0),
1540
+ twentyOnePlusThreeAmount: new BigNumber(0),
1541
+ });
1542
+ expect(payout.toNumber()).toBe(250);
1543
+ });
1544
+ });
1545
+
1546
+ describe('Blackjack.getMaxRemainingPayout', () => {
1547
+ const noSidebets = {
1548
+ perfectPairAmount: new BigNumber(0),
1549
+ twentyOnePlusThreeAmount: new BigNumber(0),
1550
+ };
1551
+
1552
+ it('is 2x the main-hand stake with no split and no double', () => {
1553
+ const payout = Blackjack.getMaxRemainingPayout({
1554
+ originalMainBetAmount: new BigNumber(10),
1555
+ mainPlayerActions: [BlackjackAction.HIT],
1556
+ splitPlayerHand: [],
1557
+ splitPlayerActions: [],
1558
+ ...noSidebets,
1559
+ });
1560
+ expect(payout.toNumber()).toBe(20);
1561
+ });
1562
+
1563
+ it('doubles the stake on a double-down (2 * 2x main bet)', () => {
1564
+ const payout = Blackjack.getMaxRemainingPayout({
1565
+ originalMainBetAmount: new BigNumber(10),
1566
+ mainPlayerActions: [BlackjackAction.DOUBLE_DOWN],
1567
+ splitPlayerHand: [],
1568
+ splitPlayerActions: [],
1569
+ ...noSidebets,
1570
+ });
1571
+ expect(payout.toNumber()).toBe(40);
1572
+ });
1573
+
1574
+ it('covers both hands after a split (2 * (main + split) stake)', () => {
1575
+ const payout = Blackjack.getMaxRemainingPayout({
1576
+ originalMainBetAmount: new BigNumber(10),
1577
+ mainPlayerActions: [BlackjackAction.SPLIT],
1578
+ splitPlayerHand: [getIndex('8'), getIndex('9')],
1579
+ splitPlayerActions: [BlackjackAction.SPLIT],
1580
+ ...noSidebets,
1581
+ });
1582
+ expect(payout.toNumber()).toBe(40);
1583
+ });
1584
+
1585
+ it('reaches 8x main bet when split and both hands double down', () => {
1586
+ const payout = Blackjack.getMaxRemainingPayout({
1587
+ originalMainBetAmount: new BigNumber(10),
1588
+ mainPlayerActions: [BlackjackAction.SPLIT, BlackjackAction.DOUBLE_DOWN],
1589
+ splitPlayerHand: [getIndex('8'), getIndex('9')],
1590
+ splitPlayerActions: [BlackjackAction.SPLIT, BlackjackAction.DOUBLE_DOWN],
1591
+ ...noSidebets,
1592
+ });
1593
+ expect(payout.toNumber()).toBe(80);
1594
+ });
1595
+
1596
+ it('adds resolved sidebet winnings on top of the hand stakes', () => {
1597
+ const payout = Blackjack.getMaxRemainingPayout({
1598
+ originalMainBetAmount: new BigNumber(10),
1599
+ mainPlayerActions: [BlackjackAction.DOUBLE_DOWN],
1600
+ splitPlayerHand: [],
1601
+ splitPlayerActions: [],
1602
+ perfectPairWin: PerfectPairType.PERFECT_PAIR, // 26x
1603
+ perfectPairAmount: new BigNumber(2),
1604
+ twentyOnePlusThreeWin: TwentyOnePlusThreeType.STRAIGHT, // 11x
1605
+ twentyOnePlusThreeAmount: new BigNumber(1),
1606
+ });
1607
+ // 2*(2*10) + 26*2 + 11*1 = 40 + 52 + 11 = 103
1608
+ expect(payout.toNumber()).toBe(103);
1609
+ });
1610
+
1611
+ it('ignores a sidebet win when its amount is zero', () => {
1612
+ const payout = Blackjack.getMaxRemainingPayout({
1613
+ originalMainBetAmount: new BigNumber(10),
1614
+ mainPlayerActions: [],
1615
+ splitPlayerHand: [],
1616
+ splitPlayerActions: [],
1617
+ perfectPairWin: PerfectPairType.PERFECT_PAIR,
1618
+ perfectPairAmount: new BigNumber(0),
1619
+ twentyOnePlusThreeAmount: new BigNumber(0),
1620
+ });
1621
+ expect(payout.toNumber()).toBe(20);
1622
+ });
1623
+ });
@@ -93,6 +93,52 @@ const suitColorMap: Record<Suits, SuitColor> = {
93
93
  };
94
94
 
95
95
  export class Blackjack {
96
+ // A natural blackjack (3:2) is the best the main bet can do at deal time
97
+ static readonly NATURAL_MAIN_BET_MULTIPLIER = new BigNumber(2.5);
98
+
99
+ static getMaxPotentialPayout(params: {
100
+ mainBetAmount: BigNumber;
101
+ perfectPairAmount: BigNumber;
102
+ twentyOnePlusThreeAmount: BigNumber;
103
+ }): BigNumber {
104
+ const maxPerfectPair = BigNumber.max(...Object.values(perfectPairPayout));
105
+ const maxTwentyOnePlusThree = BigNumber.max(...Object.values(twentyOnePlusThreePayout));
106
+ return params.mainBetAmount
107
+ .multipliedBy(Blackjack.NATURAL_MAIN_BET_MULTIPLIER)
108
+ .plus(params.perfectPairAmount.multipliedBy(maxPerfectPair))
109
+ .plus(params.twentyOnePlusThreeAmount.multipliedBy(maxTwentyOnePlusThree));
110
+ }
111
+
112
+ static getMaxRemainingPayout(action: {
113
+ originalMainBetAmount: BigNumber;
114
+ mainPlayerActions: BlackjackAction[];
115
+ splitPlayerHand: readonly number[];
116
+ splitPlayerActions: BlackjackAction[];
117
+ perfectPairWin?: PerfectPairType;
118
+ twentyOnePlusThreeWin?: TwentyOnePlusThreeType;
119
+ perfectPairAmount: BigNumber;
120
+ twentyOnePlusThreeAmount: BigNumber;
121
+ }): BigNumber {
122
+ const mainBet = new BigNumber(action.originalMainBetAmount);
123
+ const hasSplit = action.splitPlayerHand.length > 0;
124
+
125
+ const mainHandStake = mainBet.multipliedBy(action.mainPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) ? 2 : 1);
126
+ const splitHandStake = hasSplit
127
+ ? mainBet.multipliedBy(action.splitPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) ? 2 : 1)
128
+ : new BigNumber(0);
129
+
130
+ const perfectPairAmount = new BigNumber(action.perfectPairAmount);
131
+ const twentyOnePlusThreeAmount = new BigNumber(action.twentyOnePlusThreeAmount);
132
+ const perfectPairPayoutAmt =
133
+ action.perfectPairWin && perfectPairAmount.gt(0) ? perfectPairAmount.multipliedBy(perfectPairPayout[action.perfectPairWin]) : new BigNumber(0);
134
+ const twentyOnePlusThreePayoutAmt =
135
+ action.twentyOnePlusThreeWin && twentyOnePlusThreeAmount.gt(0)
136
+ ? twentyOnePlusThreeAmount.multipliedBy(twentyOnePlusThreePayout[action.twentyOnePlusThreeWin])
137
+ : new BigNumber(0);
138
+
139
+ return mainHandStake.plus(splitHandStake).multipliedBy(2).plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
140
+ }
141
+
96
142
  static availableNextActions(
97
143
  currentPlayerHand: readonly number[],
98
144
  dealerCards: [number],
@@ -156,7 +202,11 @@ export class Blackjack {
156
202
  return true;
157
203
  }
158
204
 
159
- static getHandOutcome(dealerCards: readonly number[], playerCards: readonly number[], hasSplit: boolean): { multiplier: BigNumber; outcome: HandOutcome } {
205
+ static getHandOutcome(
206
+ dealerCards: readonly number[],
207
+ playerCards: readonly number[],
208
+ hasSplit: boolean,
209
+ ): { multiplier: BigNumber; outcome: HandOutcome } {
160
210
  const dealerBlackjack = this.isBlackjack(dealerCards);
161
211
  const playerBlackjack = this.isBlackjack(playerCards);
162
212
 
@@ -194,7 +244,10 @@ export class Blackjack {
194
244
  }
195
245
  }
196
246
 
197
- static calcSideBetWins(dealerUpCard: number, playerCards: readonly number[]): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
247
+ static calcSideBetWins(
248
+ dealerUpCard: number,
249
+ playerCards: readonly number[],
250
+ ): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
198
251
  const results: { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } = {};
199
252
  const dealerCardDetails = CardGames.getCardDetails(dealerUpCard);
200
253
  const playerCard0 = CardGames.getCardDetails(playerCards[0]);
@@ -230,7 +283,9 @@ export class Blackjack {
230
283
  A: [14, 1],
231
284
  };
232
285
 
233
- const cardValues = [...cardValueMap[playerCard0.value], ...cardValueMap[playerCard1.value], ...cardValueMap[dealerCardDetails.value]].sort((a, b) => a - b);
286
+ const cardValues = [...cardValueMap[playerCard0.value], ...cardValueMap[playerCard1.value], ...cardValueMap[dealerCardDetails.value]].sort(
287
+ (a, b) => a - b,
288
+ );
234
289
 
235
290
  const isFlush = dealerCardDetails.suit === playerCard0.suit && playerCard0.suit === playerCard1.suit;
236
291
  const firstThreeCards = cardValues.slice(0, 3) as [number, number, number];
@@ -275,7 +330,7 @@ export class Blackjack {
275
330
  K: 10,
276
331
  };
277
332
 
278
- let aceCount = CardGames.getCardDetailsArr(cards).filter((card) => card.value === 'A').length;
333
+ let aceCount = CardGames.getCardDetailsArr(cards).filter(card => card.value === 'A').length;
279
334
  let cardSum = CardGames.getCardDetailsArr(cards).reduce((acc, cur) => acc + cardValueMap[cur.value], 0);
280
335
 
281
336
  if (aceCount === 0) {
@@ -345,7 +400,12 @@ export class Blackjack {
345
400
  * Called after bet ends. Players can still win insurance or sidebets even if they lose the main bet
346
401
  * @returns total payout amount
347
402
  */
348
- static getGameOutcome(data: SettleBetVars): { payout: BigNumber; multiplier: BigNumber; mainHandOutcome: HandOutcome; splitHandOutcome: HandOutcome } {
403
+ static getGameOutcome(data: SettleBetVars): {
404
+ payout: BigNumber;
405
+ multiplier: BigNumber;
406
+ mainHandOutcome: HandOutcome;
407
+ splitHandOutcome: HandOutcome;
408
+ } {
349
409
  if (data.dealerHand.length < 2) {
350
410
  throw new Error('Dealer hole card is missing');
351
411
  }
@@ -360,9 +420,15 @@ export class Blackjack {
360
420
  let perfectPairPayoutAmt = new BigNumber(0);
361
421
  let twentyOnePlusThreePayoutAmt = new BigNumber(0);
362
422
 
363
- const { mainHandOutcome, payout: mainBetPayout, splitHandOutcome } = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount });
423
+ const {
424
+ mainHandOutcome,
425
+ payout: mainBetPayout,
426
+ splitHandOutcome,
427
+ } = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount });
364
428
 
365
- const playerHand: [number, number] = data.hasSplit ? [data.mainPlayerHand[0], data.splitPlayerHand[0]] : [data.mainPlayerHand[0], data.mainPlayerHand[1]];
429
+ const playerHand: [number, number] = data.hasSplit
430
+ ? [data.mainPlayerHand[0], data.splitPlayerHand[0]]
431
+ : [data.mainPlayerHand[0], data.mainPlayerHand[1]];
366
432
 
367
433
  const { perfectPair, twentyOnePlusThree } = Blackjack.calcSideBetWins(data.dealerHand[0], playerHand);
368
434
 
@@ -376,7 +442,11 @@ export class Blackjack {
376
442
 
377
443
  const payout = mainBetPayout.plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
378
444
  const insuranceCost = data.boughtInsurance ? data.mainBetAmount.dividedBy(2) : new BigNumber(0); // insurance costs half of the main bet
379
- const totalBetAmount = mainHandBetAmount.plus(splitHandBetAmount).plus(data.perfectPairAmount).plus(data.twentyOnePlusThreeAmount).plus(insuranceCost);
445
+ const totalBetAmount = mainHandBetAmount
446
+ .plus(splitHandBetAmount)
447
+ .plus(data.perfectPairAmount)
448
+ .plus(data.twentyOnePlusThreeAmount)
449
+ .plus(insuranceCost);
380
450
 
381
451
  let multiplier = payout.dividedBy(totalBetAmount);
382
452
 
@@ -6,6 +6,7 @@ import {
6
6
  CoinflipProgressiveCoinSelectionAction,
7
7
  CoinflipProgressiveStartAction,
8
8
  } from './coinflip-classic-progressive';
9
+ import { CoinflipRules } from './coinflip-rules';
9
10
  import { CoinSide, MAX_FLIPS } from './constants';
10
11
 
11
12
  const EDGE_BPS = 200;
@@ -260,3 +261,11 @@ describe('CoinflipClassicProgressive.autobet', () => {
260
261
  expect(flipResults).toHaveLength(MAX_FLIPS);
261
262
  });
262
263
  });
264
+
265
+ describe('CoinflipClassicProgressive.getNextWinMultiplier', () => {
266
+ it('is the classic multiplier for one more successful flip', () => {
267
+ const edge = 100;
268
+ const game = CoinflipClassicProgressive.fromActions([{ phase: 'START' }]); // flipsRevealed = 0
269
+ expect(game.getNextWinMultiplier(edge).toString()).toBe(CoinflipRules.calculateClassicMultiplier(1, edge).toString());
270
+ });
271
+ });
@@ -93,6 +93,11 @@ export class CoinflipClassicProgressive {
93
93
  return { multiplier, selectedSides, flipResults: pairs.map(p => p.result) };
94
94
  }
95
95
 
96
+ public getNextWinMultiplier(edge: number): BigNumber {
97
+ const revealedAfter = Math.min(this.flipsRevealed + 1, MAX_FLIPS);
98
+ return CoinflipRules.calculateClassicMultiplier(revealedAfter, edge);
99
+ }
100
+
96
101
  public cashout(edge: number): CoinflipClassicProgressiveGameResult {
97
102
  if (!this.canCashout()) {
98
103
  throw new Error('cashout called with no flips revealed');
@@ -1,5 +1,5 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import { Hilo, HiloGuess } from './hilo';
2
+ import { Hilo, HiloAction, HiloGuess } from './hilo';
3
3
 
4
4
  describe('HiLo', () => {
5
5
  it('can check that a guess is correct', () => {
@@ -42,6 +42,59 @@ describe('HiLo', () => {
42
42
  });
43
43
  });
44
44
 
45
+ describe('calculateNextPotentialMultiplierBN', () => {
46
+ const EDGE = 100;
47
+ const action = (overrides: Partial<HiloAction> = {}): HiloAction => ({
48
+ card: null,
49
+ guess: null,
50
+ winMultiplier: null,
51
+ ...overrides,
52
+ });
53
+
54
+ it('returns 0 when there are no actions yet', () => {
55
+ expect(Hilo.calculateNextPotentialMultiplierBN([], HiloGuess.SAME_OR_ABOVE, EDGE).toString()).toEqual('0');
56
+ });
57
+
58
+ it('returns the last recorded winMultiplier when skipping', () => {
59
+ const actions = [action({ card: 20, winMultiplier: new BigNumber('2.5') })];
60
+ expect(Hilo.calculateNextPotentialMultiplierBN(actions, HiloGuess.SKIP, EDGE).toString()).toEqual('2.5');
61
+ });
62
+
63
+ it('returns 0 when skipping and no winMultiplier has been recorded', () => {
64
+ const actions = [action({ card: 20, winMultiplier: null })];
65
+ expect(Hilo.calculateNextPotentialMultiplierBN(actions, HiloGuess.SKIP, EDGE).toString()).toEqual('0');
66
+ });
67
+
68
+ it('returns 0 when the most recent card is unknown', () => {
69
+ const actions = [action({ card: 20 }), action({ card: null, winMultiplier: new BigNumber('2') })];
70
+ expect(Hilo.calculateNextPotentialMultiplierBN(actions, HiloGuess.SAME_OR_ABOVE, EDGE).toString()).toEqual('0');
71
+ });
72
+
73
+ it('computes the multiplier for the pending guess against the only (start) card', () => {
74
+ const actions = [action({ card: 20 })];
75
+ const expected = Hilo.calculateTotalMultiplierBN([{ lastCard: 20, guess: HiloGuess.SAME_OR_ABOVE }], EDGE);
76
+ expect(Hilo.calculateNextPotentialMultiplierBN(actions, HiloGuess.SAME_OR_ABOVE, EDGE).toString()).toEqual(expected.toString());
77
+ });
78
+
79
+ it('compounds the historical guesses and skips SKIP actions when building the sequence', () => {
80
+ const actions = [
81
+ action({ card: 20 }), // start card
82
+ action({ card: 8, guess: HiloGuess.SAME_OR_BELOW }), // guessed below from card 20
83
+ action({ card: 8, guess: HiloGuess.SKIP }), // skip -> excluded from the sequence
84
+ action({ card: 40, guess: HiloGuess.SAME_OR_ABOVE }), // guessed above from card 8
85
+ ];
86
+ const expected = Hilo.calculateTotalMultiplierBN(
87
+ [
88
+ { lastCard: 20, guess: HiloGuess.SAME_OR_BELOW },
89
+ { lastCard: 8, guess: HiloGuess.SAME_OR_ABOVE },
90
+ { lastCard: 40, guess: HiloGuess.SAME_OR_ABOVE }, // most recent card + pending guess
91
+ ],
92
+ EDGE,
93
+ );
94
+ expect(Hilo.calculateNextPotentialMultiplierBN(actions, HiloGuess.SAME_OR_ABOVE, EDGE).toString()).toEqual(expected.toString());
95
+ });
96
+ });
97
+
45
98
  it('calculates multiplier correctly', () => {
46
99
  // ace and same or above
47
100
  expect(Hilo.calculateWinChanceBN(HiloGuess.SAME_OR_ABOVE, 51).toString()).toEqual(new BigNumber(12).dividedBy(13).toString());
package/src/games/hilo.ts CHANGED
@@ -28,6 +28,12 @@ export enum Suits {
28
28
  CLUBS = 'CLUBS',
29
29
  }
30
30
 
31
+ export type HiloAction = {
32
+ card: number | null;
33
+ guess: HiloGuess | null;
34
+ winMultiplier: BigNumber | number | null;
35
+ };
36
+
31
37
  export class Hilo {
32
38
  static isGuessCorrect(previousCard: number, guess: HiloGuess, result: number) {
33
39
  const previousCardValue = CardGames.toComparableNo(previousCard);
@@ -61,7 +67,7 @@ export class Hilo {
61
67
 
62
68
  static calculateTotalMultiplierBN(actions: { guess: HiloGuess; lastCard: number }[], edgeBps: number) {
63
69
  const totalWinChance = actions
64
- .map((action) => this.calculateWinChanceBN(action.guess, action.lastCard))
70
+ .map(action => this.calculateWinChanceBN(action.guess, action.lastCard))
65
71
  .reduce((prev, curr) => prev.multipliedBy(curr), BigNumber(1));
66
72
 
67
73
  if (totalWinChance.isZero()) {
@@ -70,6 +76,34 @@ export class Hilo {
70
76
  return calculateEdgeMultiplier(edgeBps).div(totalWinChance).decimalPlaces(4);
71
77
  }
72
78
 
79
+ static calculateNextPotentialMultiplierBN(hiloActions: HiloAction[], guess: HiloGuess, edgeBps: number): BigNumber {
80
+ if (hiloActions.length === 0) {
81
+ return new BigNumber(0);
82
+ }
83
+
84
+ if (guess === HiloGuess.SKIP) {
85
+ const last = hiloActions[hiloActions.length - 1];
86
+ return new BigNumber(last?.winMultiplier ?? 0);
87
+ }
88
+
89
+ const lastCardAndGuessActions: { lastCard: number; guess: HiloGuess }[] = [];
90
+ for (let i = 0; i < hiloActions.length - 1; i++) {
91
+ const current = hiloActions[i];
92
+ const next = hiloActions[i + 1];
93
+ if (current && next && next.guess !== HiloGuess.SKIP && next.guess !== null && current.card !== null) {
94
+ lastCardAndGuessActions.push({ lastCard: current.card, guess: next.guess });
95
+ }
96
+ }
97
+
98
+ const lastHiloAction = hiloActions[hiloActions.length - 1];
99
+ if (!lastHiloAction || lastHiloAction.card === null) {
100
+ return new BigNumber(0);
101
+ }
102
+ lastCardAndGuessActions.push({ lastCard: lastHiloAction.card, guess });
103
+
104
+ return this.calculateTotalMultiplierBN(lastCardAndGuessActions, edgeBps);
105
+ }
106
+
73
107
  static calculateMultiplier(guess: HiloGuess, previousCard: number, edgeBps: number): number {
74
108
  const result = this.calculateMultiplierBN(guess, previousCard, edgeBps);
75
109
 
@@ -1,5 +1,5 @@
1
1
  import { createHmac } from 'crypto';
2
- import { Keno } from './keno';
2
+ import { Keno, KenoRiskLevel } from './keno';
3
3
 
4
4
  describe('Keno', () => {
5
5
  it('be able to provably result', () => {
@@ -86,3 +86,22 @@ test('should contain the same length for each config', () => {
86
86
  });
87
87
  });
88
88
 
89
+ describe('Keno.getMaxMultiplier', () => {
90
+ it('returns the largest payout for the given tiles/risk across every config', () => {
91
+ Keno.configs().forEach((config) => {
92
+ (Object.keys(config.payout) as KenoRiskLevel[]).forEach((risk) => {
93
+ const max = Keno.getMaxMultiplier(risk, config.tiles);
94
+ expect(max.toNumber()).toBe(Math.max(...config.payout[risk]));
95
+ });
96
+ });
97
+ });
98
+
99
+ it('picks a concrete known maximum (10 tiles, high risk)', () => {
100
+ expect(Keno.getMaxMultiplier(KenoRiskLevel.HIGH_RISK, 10).toNumber()).toBe(1000);
101
+ });
102
+
103
+ it('throws when no configuration exists for the tile count', () => {
104
+ expect(() => Keno.getMaxMultiplier(KenoRiskLevel.CLASSIC, 11)).toThrow('No configuration found for 11 tiles');
105
+ });
106
+ });
107
+
package/src/games/keno.ts CHANGED
@@ -116,6 +116,14 @@ export class Keno {
116
116
  return kenoConfigs;
117
117
  }
118
118
 
119
+ static getMaxMultiplier(risk: KenoRiskLevel, tiles: number): BigNumber {
120
+ const config = this.configs().find(config => config.tiles === tiles);
121
+ if (!config) {
122
+ throw new Error(`No configuration found for ${tiles} tiles`);
123
+ }
124
+ return BigNumber.max(...config.payout[risk].map(v => new BigNumber(v)));
125
+ }
126
+
119
127
  static calculateMultiplier(risk: KenoRiskLevel, selectedTiles: number[], results: number[]) {
120
128
  const matchedTiles = results.filter(result => selectedTiles.includes(result));
121
129
  const config = this.configs().find(config => config.tiles === selectedTiles.length);
@@ -78,6 +78,25 @@ describe('Plinko', () => {
78
78
  }
79
79
  });
80
80
 
81
+ describe('Plinko.getMaxMultiplier', () => {
82
+ it('returns the largest payout for the given rows/risk across every config', () => {
83
+ Plinko.configs().forEach((config) => {
84
+ (Object.keys(config.payout) as PlinkoRiskLevel[]).forEach((risk) => {
85
+ const max = Plinko.getMaxMultiplier(risk, config.rows);
86
+ expect(max.toNumber()).toBe(Math.max(...config.payout[risk]));
87
+ });
88
+ });
89
+ });
90
+
91
+ it('picks a concrete known maximum (16 rows, high risk)', () => {
92
+ expect(Plinko.getMaxMultiplier(PlinkoRiskLevel.HIGH_RISK, 16).toNumber()).toBe(1000);
93
+ });
94
+
95
+ it('throws when no configuration exists for the row count', () => {
96
+ expect(() => Plinko.getMaxMultiplier(PlinkoRiskLevel.LOW_RISK, 7)).toThrow('No configuration found for 7 rows');
97
+ });
98
+ });
99
+
81
100
  function generateDigestHex({
82
101
  serverSeed,
83
102
  clientSeed,
@@ -122,6 +122,14 @@ export class Plinko {
122
122
  );
123
123
  }
124
124
 
125
+ static getMaxMultiplier(risk: PlinkoRiskLevel, rows: number): BigNumber {
126
+ const config = this.configs().find(config => config.rows === rows);
127
+ if (!config) {
128
+ throw new Error(`No configuration found for ${rows} rows`);
129
+ }
130
+ return BigNumber.max(...config.payout[risk].map(v => new BigNumber(v)));
131
+ }
132
+
125
133
  static calculateMultiplier(risk: PlinkoRiskLevel, rows: number, results: number[]) {
126
134
  const position = results.reduce((sum, result) => sum + result, 0);
127
135
  const config = this.configs().find(config => config.rows === rows);