shufflecom-calculations 1.2.1 → 1.2.3

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.
@@ -11,6 +11,15 @@ export enum BlackjackAction {
11
11
  REJECT_INSURANCE = 'REJECT_INSURANCE',
12
12
  }
13
13
 
14
+ export enum HandOutcome {
15
+ WIN = 'WIN', // refers to a win that pays out 2:1. Blackjacks after splits are still considered a HandOutcome.WIN and not HandOutcome.BLACKJACK as the payout is 2:1
16
+ LOSS = 'LOSS',
17
+ PUSH = 'PUSH',
18
+ BLACKJACK = 'BLACKJACK', // split hand can NEVER have a HandOutcome.BLACKJACK as the payout is 2:1 so it's a HandOutcome.WIN
19
+ PENDING = 'PENDING',
20
+ NONE = 'NONE', // for split player hand if the user has not split
21
+ }
22
+
14
23
  export enum PerfectPairType {
15
24
  PERFECT_PAIR = 'PERFECT_PAIR',
16
25
  COLORED_PAIR = 'COLORED_PAIR',
@@ -34,15 +43,26 @@ export enum InsuranceStatus {
34
43
  }
35
44
 
36
45
  export interface SettleBetVars {
37
- mainPlayerHand: number[];
38
- splitPlayerHand: number[];
39
- dealerHand: number[];
40
- mainHandBetAmount: BigNumber;
41
- splitHandBetAmount: BigNumber;
46
+ mainPlayerHand: readonly number[];
47
+ splitPlayerHand: readonly number[];
48
+ dealerHand: readonly number[];
49
+ mainBetAmount: BigNumber; // the amount that the user entered when they placed the main bet
42
50
  twentyOnePlusThreeAmount: BigNumber;
43
51
  perfectPairAmount: BigNumber;
44
52
  boughtInsurance: boolean;
45
53
  hasSplit: boolean;
54
+ mainHandDoubleDown: boolean;
55
+ splitHandDoubleDown: boolean;
56
+ }
57
+
58
+ interface MainBetPayoutVars {
59
+ mainPlayerHand: readonly number[];
60
+ splitPlayerHand: readonly number[];
61
+ dealerHand: readonly number[];
62
+ mainHandBetAmount: BigNumber;
63
+ splitHandBetAmount: BigNumber;
64
+ boughtInsurance: boolean;
65
+ hasSplit: boolean;
46
66
  }
47
67
 
48
68
  // Since the values are payout and not profit, it's the profit multiple + 1
@@ -73,8 +93,8 @@ const suitColorMap: Record<Suits, SuitColor> = {
73
93
 
74
94
  export class Blackjack {
75
95
  static availableNextActions(
76
- currentPlayerHand: number[],
77
- dealerCards: number[],
96
+ currentPlayerHand: readonly number[],
97
+ dealerCards: [number],
78
98
  insuranceStatus: InsuranceStatus,
79
99
  atMainHandAndHasNotSplit: boolean,
80
100
  ): BlackjackAction[] {
@@ -103,8 +123,8 @@ export class Blackjack {
103
123
  static playingMainHand(
104
124
  mainPlayerCards: number[],
105
125
  splitPlayerCards: number[],
106
- mainPlayerActions: BlackjackAction[],
107
- splitPlayerActions: BlackjackAction[],
126
+ mainPlayerActions: readonly BlackjackAction[],
127
+ splitPlayerActions: readonly BlackjackAction[],
108
128
  ): boolean {
109
129
  // still on main hand if main hand < 21 and user has not chosen to double down or stand (as these are the 2 actions that will end the main hand)
110
130
  if (
@@ -132,48 +152,48 @@ export class Blackjack {
132
152
 
133
153
  // main hand busted/ended and no split hand
134
154
  // OR main hand busted and split hand busted/ended
135
- throw new Error('No available action');
155
+ return true;
136
156
  }
137
157
 
138
- static calcMainBetMultiplier(dealerCards: number[], playerCards: number[], hasSplit: boolean): BigNumber {
139
- const dealerBlackjack = this.isBlackjack(dealerCards.slice(0, 2));
140
- const playerBlackjack = this.isBlackjack(playerCards.slice(0, 2));
158
+ static getHandOutcome(dealerCards: readonly number[], playerCards: readonly number[], hasSplit: boolean): { multiplier: BigNumber; outcome: HandOutcome } {
159
+ const dealerBlackjack = this.isBlackjack(dealerCards);
160
+ const playerBlackjack = this.isBlackjack(playerCards);
141
161
 
142
162
  if (playerBlackjack && !dealerBlackjack && !hasSplit) {
143
- return new BigNumber(2.5);
163
+ return { multiplier: new BigNumber(2.5), outcome: HandOutcome.BLACKJACK };
144
164
  }
145
165
 
146
166
  if (dealerBlackjack && !playerBlackjack) {
147
- return new BigNumber(0);
167
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
148
168
  }
149
169
 
150
170
  if (dealerBlackjack && playerBlackjack) {
151
- return new BigNumber(1);
171
+ return { multiplier: new BigNumber(1), outcome: HandOutcome.PUSH };
152
172
  }
153
173
 
154
174
  // If it reaches here, neither player nor dealer has blackjack
155
175
  // if player busted, dealer auto wins
156
176
  if (this.hasBusted(playerCards)) {
157
- return new BigNumber(0);
177
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
158
178
  }
159
179
 
160
180
  if (this.hasBusted(dealerCards)) {
161
- return new BigNumber(2);
181
+ return { multiplier: new BigNumber(2), outcome: HandOutcome.WIN };
162
182
  }
163
183
 
164
184
  // Both did not bust, so compare the sum of cards
165
185
  const playerSum = this.calcHandValue(playerCards);
166
186
  const dealerSum = this.calcHandValue(dealerCards);
167
187
  if (playerSum > dealerSum) {
168
- return new BigNumber(2);
188
+ return { multiplier: new BigNumber(2), outcome: HandOutcome.WIN };
169
189
  } else if (playerSum < dealerSum) {
170
- return new BigNumber(0);
190
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
171
191
  } else {
172
- return new BigNumber(1);
192
+ return { multiplier: new BigNumber(1), outcome: HandOutcome.PUSH };
173
193
  }
174
194
  }
175
195
 
176
- static calcSideBetWins(dealerUpCard: number, playerCards: number[]): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
196
+ static calcSideBetWins(dealerUpCard: number, playerCards: readonly number[]): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
177
197
  const results: { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } = {};
178
198
  const dealerCardDetails = CardGames.getCardDetails(dealerUpCard);
179
199
  const playerCard0 = CardGames.getCardDetails(playerCards[0]);
@@ -236,7 +256,7 @@ export class Blackjack {
236
256
  return results;
237
257
  }
238
258
 
239
- static calcHandValue(cards: number[]): number {
259
+ static calcHandValue(cards: readonly number[]): number {
240
260
  // taking Ace as 11 here
241
261
  const cardValueMap: Record<CardValue, number> = {
242
262
  '2': 2,
@@ -271,21 +291,21 @@ export class Blackjack {
271
291
  return cardSum;
272
292
  }
273
293
 
274
- static canBuyInsurance(dealerCards: number[]): boolean {
294
+ static canBuyInsurance(dealerCards: readonly number[]): boolean {
275
295
  // player can only buy insurance if dealer's first card(upcard) is an ace
276
296
  return CardGames.getCardDetails(dealerCards[0]).value === 'A';
277
297
  }
278
298
 
279
299
  // Dealer does NOT hit on soft 17 (for our implementation)
280
- static shouldDealerHit(dealerCards: number[]): boolean {
300
+ static shouldDealerHit(dealerCards: readonly number[]): boolean {
281
301
  return this.calcHandValue(dealerCards) < 17;
282
302
  }
283
303
 
284
- static hasBusted(cards: number[]): boolean {
304
+ private static hasBusted(cards: readonly number[]): boolean {
285
305
  return this.calcHandValue(cards) > 21;
286
306
  }
287
307
 
288
- static canSplit(mainHand: number[]): boolean {
308
+ private static canSplit(mainHand: readonly number[]): boolean {
289
309
  if (mainHand.length !== 2) {
290
310
  return false;
291
311
  }
@@ -293,11 +313,11 @@ export class Blackjack {
293
313
  return cardActions[0].value === cardActions[1].value;
294
314
  }
295
315
 
296
- static canDoubleDown(playerCards: number[]): boolean {
316
+ private static canDoubleDown(playerCards: readonly number[]): boolean {
297
317
  return playerCards.length === 2;
298
318
  }
299
319
 
300
- static isBlackjack(cards: number[]): boolean {
320
+ static isBlackjack(cards: readonly number[]): boolean {
301
321
  if (cards.length !== 2) {
302
322
  return false;
303
323
  }
@@ -305,48 +325,98 @@ export class Blackjack {
305
325
  return this.calcHandValue(cards) === 21;
306
326
  }
307
327
 
328
+ // TODO: Write unit tests for this
329
+ static playDealerHand(dealerHand: readonly number[], numberOfCardsDealt: number, deck: readonly number[]): number[] {
330
+ const newDealerHand = [...dealerHand];
331
+ // while the dealer doesn't have 17 or more, draw a card
332
+ while (Blackjack.shouldDealerHit(newDealerHand)) {
333
+ newDealerHand.push(deck[numberOfCardsDealt++]);
334
+ }
335
+
336
+ return newDealerHand;
337
+ }
338
+
308
339
  /**
309
340
  *
310
341
  * Called after bet ends. Players can still win insurance or sidebets even if they lose the main bet
311
342
  * @returns total payout amount
312
343
  */
313
- static getPayout({
314
- mainPlayerHand,
315
- splitPlayerHand,
316
- dealerHand,
317
- mainHandBetAmount,
318
- splitHandBetAmount,
319
- twentyOnePlusThreeAmount,
320
- // TODO: Include 2 more fields for sidebet win types
321
- perfectPairAmount,
322
- boughtInsurance,
323
- hasSplit,
324
- }: SettleBetVars): BigNumber {
344
+ static getGameOutcome(data: SettleBetVars): { payout: BigNumber; multiplier: BigNumber; mainHandOutcome: HandOutcome; splitHandOutcome: HandOutcome } {
345
+ if (data.dealerHand.length < 2) {
346
+ throw new Error('Dealer hole card is missing');
347
+ }
325
348
  // calculate main bet payout for main and split hand
326
-
327
- const mainBetPayout = Blackjack.calcMainBetMultiplier(dealerHand, mainPlayerHand, hasSplit).times(mainHandBetAmount);
328
- const splitBetPayout =
329
- splitPlayerHand.length > 0 ? Blackjack.calcMainBetMultiplier(dealerHand, splitPlayerHand, hasSplit).times(splitHandBetAmount) : new BigNumber(0);
330
-
331
- // calculate insurance payout (insurance payout is 3:1)
332
- const insurancePayout = boughtInsurance && Blackjack.isBlackjack(dealerHand) ? mainHandBetAmount.dividedBy(2).times(3) : new BigNumber(0);
349
+ const mainHandBetAmount = data.mainHandDoubleDown ? data.mainBetAmount.times(2) : data.mainBetAmount;
350
+ let splitHandBetAmount = new BigNumber(0);
351
+ if (data.hasSplit) {
352
+ splitHandBetAmount = data.splitHandDoubleDown ? data.mainBetAmount.times(2) : data.mainBetAmount;
353
+ }
333
354
 
334
355
  // calculate sidebet payouts
335
356
  let perfectPairPayoutAmt = new BigNumber(0);
336
357
  let twentyOnePlusThreePayoutAmt = new BigNumber(0);
337
358
 
338
- const playerHand: [number, number] = hasSplit ? [mainPlayerHand[0], splitPlayerHand[0]] : [mainPlayerHand[0], mainPlayerHand[1]];
359
+ const { mainHandOutcome, payout: mainBetPayout, splitHandOutcome } = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount });
360
+
361
+ const playerHand: [number, number] = data.hasSplit ? [data.mainPlayerHand[0], data.splitPlayerHand[0]] : [data.mainPlayerHand[0], data.mainPlayerHand[1]];
339
362
 
340
- const { perfectPair, twentyOnePlusThree } = Blackjack.calcSideBetWins(dealerHand[0], playerHand);
363
+ const { perfectPair, twentyOnePlusThree } = Blackjack.calcSideBetWins(data.dealerHand[0], playerHand);
341
364
 
342
- if (perfectPairPayout[perfectPair] && perfectPairAmount.gt(0)) {
343
- perfectPairPayoutAmt = perfectPairAmount.times(perfectPairPayout[perfectPair]);
365
+ if (perfectPairPayout[perfectPair] && data.perfectPairAmount.gt(0)) {
366
+ perfectPairPayoutAmt = data.perfectPairAmount.times(perfectPairPayout[perfectPair]);
344
367
  }
345
368
 
346
- if (twentyOnePlusThreePayout[twentyOnePlusThree] && twentyOnePlusThreeAmount.gt(0)) {
347
- twentyOnePlusThreePayoutAmt = twentyOnePlusThreeAmount.times(twentyOnePlusThreePayout[twentyOnePlusThree]);
369
+ if (twentyOnePlusThreePayout[twentyOnePlusThree] && data.twentyOnePlusThreeAmount.gt(0)) {
370
+ twentyOnePlusThreePayoutAmt = data.twentyOnePlusThreeAmount.times(twentyOnePlusThreePayout[twentyOnePlusThree]);
348
371
  }
349
372
 
350
- return mainBetPayout.plus(splitBetPayout).plus(insurancePayout).plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
373
+ const payout = mainBetPayout.plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
374
+ const insuranceCost = data.boughtInsurance ? data.mainBetAmount.dividedBy(2) : new BigNumber(0); // insurance costs half of the main bet
375
+ const totalBetAmount = mainHandBetAmount.plus(splitHandBetAmount).plus(data.perfectPairAmount).plus(data.twentyOnePlusThreeAmount).plus(insuranceCost);
376
+
377
+ let multiplier = payout.dividedBy(totalBetAmount);
378
+
379
+ // when the user bets $0, assume an initial bet of $1 to calculate the multiplier
380
+ if (totalBetAmount.isZero()) {
381
+ const mainHandBetAmount = data.mainHandDoubleDown ? new BigNumber(2) : new BigNumber(1);
382
+ let splitHandBetAmount = new BigNumber(0);
383
+ if (data.hasSplit) {
384
+ splitHandBetAmount = data.splitHandDoubleDown ? new BigNumber(2) : new BigNumber(1);
385
+ }
386
+
387
+ const insuranceCost = data.boughtInsurance ? new BigNumber(0.5) : new BigNumber(0); // insurance costs half of the main bet
388
+ const totalBetAmount = mainHandBetAmount.plus(splitHandBetAmount).plus(insuranceCost);
389
+
390
+ multiplier = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount }).payout.dividedBy(totalBetAmount);
391
+ }
392
+
393
+ return { payout, multiplier, mainHandOutcome, splitHandOutcome };
394
+ }
395
+
396
+ private static getMainBetPayout({
397
+ mainPlayerHand,
398
+ splitPlayerHand,
399
+ dealerHand,
400
+ mainHandBetAmount,
401
+ splitHandBetAmount,
402
+ boughtInsurance,
403
+ hasSplit,
404
+ }: MainBetPayoutVars) {
405
+ const { multiplier: mainHandMultiplier, outcome: mainHandOutcome } = Blackjack.getHandOutcome(dealerHand, mainPlayerHand, hasSplit);
406
+ const mainHandPayout = mainHandMultiplier.times(mainHandBetAmount);
407
+
408
+ const { multiplier: splitHandMultiplier, outcome: splitHandOutcome } = Blackjack.getHandOutcome(dealerHand, splitPlayerHand, hasSplit);
409
+
410
+ const splitHandPayout = hasSplit ? splitHandMultiplier.times(splitHandBetAmount) : new BigNumber(0);
411
+
412
+ // calculate insurance payout (insurance payout is 3:1)
413
+ // insurance won't be claimed if double down occurs, so double downs won't affect insurance payout even though it doubles the mainHandBetAmount
414
+ const insurancePayout = boughtInsurance && Blackjack.isBlackjack(dealerHand) ? mainHandBetAmount.dividedBy(2).times(3) : new BigNumber(0);
415
+
416
+ return {
417
+ mainHandOutcome,
418
+ splitHandOutcome: hasSplit ? splitHandOutcome : HandOutcome.NONE,
419
+ payout: mainHandPayout.plus(splitHandPayout).plus(insurancePayout),
420
+ };
351
421
  }
352
422
  }
@@ -52,7 +52,7 @@ export class CardGames {
52
52
  return { suit, value: value as CardValue };
53
53
  }
54
54
 
55
- static getCardDetailsArr(cardNums: number[]): CardDetail[] {
55
+ static getCardDetailsArr(cardNums: readonly number[]): CardDetail[] {
56
56
  return cardNums.map((cardNum) => this.getCardDetails(cardNum));
57
57
  }
58
58
 
@@ -105,3 +105,58 @@ export class CardGames {
105
105
  // ]
106
106
 
107
107
  // A = 1, 2 = 2 ,...10 = 10, J = 11, Q = 12, K = 13
108
+ /**
109
+ * 0: ♦2
110
+ * 1: ♥2
111
+ * 2: ♠2
112
+ * 3: ♣2
113
+ * 4: ♦3
114
+ * 5: ♥3
115
+ * 6: ♠3
116
+ * 7: ♣3
117
+ * 8: ♦4
118
+ * 9: ♥4
119
+ * 10: ♠4
120
+ * 11: ♣4
121
+ * 12: ♦5
122
+ * 13: ♥5
123
+ * 14: ♠5
124
+ * 15: ♣5
125
+ * 16: ♦6
126
+ * 17: ♥6
127
+ * 18: ♠6
128
+ * 19: ♣6
129
+ * 20: ♦7
130
+ * 21: ♥7
131
+ * 22: ♠7
132
+ * 23: ♣7
133
+ * 24: ♦8
134
+ * 25: ♥8
135
+ * 26: ♠8
136
+ * 27: ♣8
137
+ * 28: ♦9
138
+ * 29: ♥9
139
+ * 30: ♠9
140
+ * 31: ♣9
141
+ * 32: ♦10
142
+ * 33: ♥10
143
+ * 34: ♠10
144
+ * 35: ♣10
145
+ * 36: ♦J
146
+ * 37: ♥J
147
+ * 38: ♠J
148
+ * 39: ♣J
149
+ * 40: ♦Q
150
+ * 41: ♥Q
151
+ * 42: ♠Q
152
+ * 43: ♣Q
153
+ * 44: ♦K
154
+ * 45: ♥K
155
+ * 46: ♠K
156
+ * 47: ♣K
157
+ * 48: ♦A
158
+ * 49: ♥A
159
+ * 50: ♠A
160
+ * 51: ♣A
161
+ *
162
+ */
package/src/games/hilo.ts CHANGED
@@ -4,7 +4,6 @@ import { CardGames } from './cardGames';
4
4
 
5
5
  const TOTAL_CARD = new BigNumber(13);
6
6
 
7
- // TODO: Remove the card detail stuff from here
8
7
  /**
9
8
  * For Ace card, since it's the lowest card,
10
9
  * - SAME_OR_ABOVE -> ABOVE
@@ -29,11 +28,6 @@ export enum Suits {
29
28
  CLUBS = 'CLUBS',
30
29
  }
31
30
 
32
- export interface CardDetail {
33
- suit: Suits;
34
- value: string;
35
- }
36
-
37
31
  export class Hilo {
38
32
  static isGuessCorrect(previousCard: number, guess: HiloGuess, result: number) {
39
33
  const previousCardValue = CardGames.toComparableNo(previousCard);
package/src/index.ts CHANGED
@@ -16,4 +16,5 @@ export {
16
16
  TwentyOnePlusThreeType,
17
17
  perfectPairPayout,
18
18
  twentyOnePlusThreePayout,
19
+ HandOutcome,
19
20
  } from './games/blackjack';