shufflecom-calculations 1.2.0 → 1.2.2

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.
@@ -0,0 +1,408 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { CardGames, CardValue, Suits } from './cardGames';
3
+
4
+ export enum BlackjackAction {
5
+ HIT = 'HIT',
6
+ STAND = 'STAND',
7
+ DOUBLE_DOWN = 'DOUBLE_DOWN',
8
+ SPLIT = 'SPLIT',
9
+ // player has to select one or the other before they can continue
10
+ BUY_INSURANCE = 'BUY_INSURANCE',
11
+ REJECT_INSURANCE = 'REJECT_INSURANCE',
12
+ }
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
+
23
+ export enum PerfectPairType {
24
+ PERFECT_PAIR = 'PERFECT_PAIR',
25
+ COLORED_PAIR = 'COLORED_PAIR',
26
+ MIXED_PAIR = 'MIXED_PAIR',
27
+ }
28
+ export enum TwentyOnePlusThreeType {
29
+ SUITED_TRIPS = 'SUITED_TRIPS',
30
+ STRAIGHT_FLUSH = 'STRAIGHT_FLUSH',
31
+ STRAIGHT = 'STRAIGHT',
32
+ THREE_OF_A_KIND = 'THREE_OF_A_KIND',
33
+ FLUSH = 'FLUSH',
34
+ }
35
+
36
+ export enum InsuranceStatus {
37
+ INELIGIBLE = 'INELIGIBLE',
38
+ BOUGHT_PAYS_OUT = 'BOUGHT_PAYS_OUT',
39
+ BOUGHT_DOES_NOT_PAY_OUT = 'BOUGHT_DOES_NOT_PAY_OUT',
40
+ REJECTED = 'REJECTED',
41
+ // if in eligible stage, user must choose to buy or not buy
42
+ ELIGIBLE = 'ELIGIBLE',
43
+ }
44
+
45
+ export interface SettleBetVars {
46
+ mainPlayerHand: number[];
47
+ splitPlayerHand: number[];
48
+ dealerHand: number[];
49
+ mainBetAmount: BigNumber; // the amount that the user entered when they placed the main bet
50
+ twentyOnePlusThreeAmount: BigNumber;
51
+ perfectPairAmount: BigNumber;
52
+ boughtInsurance: boolean;
53
+ hasSplit: boolean;
54
+ mainHandDoubleDown: boolean;
55
+ splitHandDoubleDown: boolean;
56
+ }
57
+
58
+ interface MainBetPayoutVars {
59
+ mainPlayerHand: number[];
60
+ splitPlayerHand: number[];
61
+ dealerHand: number[];
62
+ mainHandBetAmount: BigNumber;
63
+ splitHandBetAmount: BigNumber;
64
+ boughtInsurance: boolean;
65
+ hasSplit: boolean;
66
+ }
67
+
68
+ // Since the values are payout and not profit, it's the profit multiple + 1
69
+ export const perfectPairPayout: Record<PerfectPairType, BigNumber> = {
70
+ [PerfectPairType.PERFECT_PAIR]: new BigNumber(26),
71
+ [PerfectPairType.COLORED_PAIR]: new BigNumber(13),
72
+ [PerfectPairType.MIXED_PAIR]: new BigNumber(7),
73
+ };
74
+
75
+ export const twentyOnePlusThreePayout: Record<TwentyOnePlusThreeType, BigNumber> = {
76
+ [TwentyOnePlusThreeType.SUITED_TRIPS]: new BigNumber(101),
77
+ [TwentyOnePlusThreeType.STRAIGHT_FLUSH]: new BigNumber(41),
78
+ [TwentyOnePlusThreeType.THREE_OF_A_KIND]: new BigNumber(31),
79
+ [TwentyOnePlusThreeType.STRAIGHT]: new BigNumber(11),
80
+ [TwentyOnePlusThreeType.FLUSH]: new BigNumber(6),
81
+ };
82
+
83
+ enum SuitColor {
84
+ RED = 'RED',
85
+ BLACK = 'BLACK',
86
+ }
87
+ const suitColorMap: Record<Suits, SuitColor> = {
88
+ [Suits.DIAMONDS]: SuitColor.RED,
89
+ [Suits.HEARTS]: SuitColor.RED,
90
+ [Suits.SPADES]: SuitColor.BLACK,
91
+ [Suits.CLUBS]: SuitColor.BLACK,
92
+ };
93
+
94
+ export class Blackjack {
95
+ static availableNextActions(
96
+ currentPlayerHand: number[],
97
+ dealerCards: number[],
98
+ insuranceStatus: InsuranceStatus,
99
+ atMainHandAndHasNotSplit: boolean,
100
+ ): BlackjackAction[] {
101
+ // User has to choose either one before they can continue
102
+ if (this.canBuyInsurance(dealerCards) && insuranceStatus === InsuranceStatus.ELIGIBLE) {
103
+ return [BlackjackAction.BUY_INSURANCE, BlackjackAction.REJECT_INSURANCE];
104
+ }
105
+
106
+ if (this.calcHandValue(currentPlayerHand) >= 21) {
107
+ return [];
108
+ }
109
+
110
+ const actions: BlackjackAction[] = [BlackjackAction.HIT, BlackjackAction.STAND];
111
+
112
+ if (this.canDoubleDown(currentPlayerHand)) {
113
+ actions.push(BlackjackAction.DOUBLE_DOWN);
114
+ }
115
+
116
+ if (this.canSplit(currentPlayerHand) && atMainHandAndHasNotSplit) {
117
+ actions.push(BlackjackAction.SPLIT);
118
+ }
119
+
120
+ return actions;
121
+ }
122
+
123
+ static playingMainHand(
124
+ mainPlayerCards: number[],
125
+ splitPlayerCards: number[],
126
+ mainPlayerActions: BlackjackAction[],
127
+ splitPlayerActions: BlackjackAction[],
128
+ ): boolean {
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)
130
+ if (
131
+ this.calcHandValue(mainPlayerCards) < 21 &&
132
+ !mainPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) &&
133
+ !mainPlayerActions.includes(BlackjackAction.STAND)
134
+ ) {
135
+ return true;
136
+ }
137
+
138
+ // on split hand if player has split and split hand is not yet over
139
+ if (
140
+ splitPlayerCards.length > 0 &&
141
+ this.calcHandValue(splitPlayerCards) < 21 &&
142
+ !splitPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) &&
143
+ !splitPlayerActions.includes(BlackjackAction.STAND)
144
+ ) {
145
+ return false;
146
+ }
147
+
148
+ // player blackjack and dealer upcard A, so user is choosing whether to buy insurance in this case, and will end immediately after this action
149
+ if (mainPlayerCards.length === 2 && this.calcHandValue(mainPlayerCards) === 21 && splitPlayerCards.length === 0) {
150
+ return true;
151
+ }
152
+
153
+ // main hand busted/ended and no split hand
154
+ // OR main hand busted and split hand busted/ended
155
+ throw new Error('No available action');
156
+ }
157
+
158
+ static getHandOutcome(dealerCards: number[], playerCards: number[], hasSplit: boolean): { multiplier: BigNumber; outcome: HandOutcome } {
159
+ const dealerBlackjack = this.isBlackjack(dealerCards);
160
+ const playerBlackjack = this.isBlackjack(playerCards);
161
+
162
+ if (playerBlackjack && !dealerBlackjack && !hasSplit) {
163
+ return { multiplier: new BigNumber(2.5), outcome: HandOutcome.BLACKJACK };
164
+ }
165
+
166
+ if (dealerBlackjack && !playerBlackjack) {
167
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
168
+ }
169
+
170
+ if (dealerBlackjack && playerBlackjack) {
171
+ return { multiplier: new BigNumber(1), outcome: HandOutcome.PUSH };
172
+ }
173
+
174
+ // If it reaches here, neither player nor dealer has blackjack
175
+ // if player busted, dealer auto wins
176
+ if (this.hasBusted(playerCards)) {
177
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
178
+ }
179
+
180
+ if (this.hasBusted(dealerCards)) {
181
+ return { multiplier: new BigNumber(2), outcome: HandOutcome.WIN };
182
+ }
183
+
184
+ // Both did not bust, so compare the sum of cards
185
+ const playerSum = this.calcHandValue(playerCards);
186
+ const dealerSum = this.calcHandValue(dealerCards);
187
+ if (playerSum > dealerSum) {
188
+ return { multiplier: new BigNumber(2), outcome: HandOutcome.WIN };
189
+ } else if (playerSum < dealerSum) {
190
+ return { multiplier: new BigNumber(0), outcome: HandOutcome.LOSS };
191
+ } else {
192
+ return { multiplier: new BigNumber(1), outcome: HandOutcome.PUSH };
193
+ }
194
+ }
195
+
196
+ static calcSideBetWins(dealerUpCard: number, playerCards: number[]): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
197
+ const results: { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } = {};
198
+ const dealerCardDetails = CardGames.getCardDetails(dealerUpCard);
199
+ const playerCard0 = CardGames.getCardDetails(playerCards[0]);
200
+ const playerCard1 = CardGames.getCardDetails(playerCards[1]);
201
+
202
+ // Perfect pairs
203
+ if (playerCard0.value === playerCard1.value) {
204
+ const cardSuits = [playerCard0.suit, playerCard1.suit] as const;
205
+
206
+ if (playerCards[0] === playerCards[1]) {
207
+ results.perfectPair = PerfectPairType.PERFECT_PAIR;
208
+ } else if (suitColorMap[cardSuits[0]] === suitColorMap[cardSuits[1]]) {
209
+ results.perfectPair = PerfectPairType.COLORED_PAIR;
210
+ } else {
211
+ results.perfectPair = PerfectPairType.MIXED_PAIR;
212
+ }
213
+ }
214
+
215
+ // 21+3
216
+ const cardValueMap: Record<CardValue, number[]> = {
217
+ '2': [2],
218
+ '3': [3],
219
+ '4': [4],
220
+ '5': [5],
221
+ '6': [6],
222
+ '7': [7],
223
+ '8': [8],
224
+ '9': [9],
225
+ '10': [10],
226
+ J: [11],
227
+ Q: [12],
228
+ K: [13],
229
+ A: [14, 1],
230
+ };
231
+
232
+ const cardValues = [...cardValueMap[playerCard0.value], ...cardValueMap[playerCard1.value], ...cardValueMap[dealerCardDetails.value]].sort((a, b) => a - b);
233
+
234
+ const isFlush = dealerCardDetails.suit === playerCard0.suit && playerCard0.suit === playerCard1.suit;
235
+ const firstThreeCards = cardValues.slice(0, 3) as [number, number, number];
236
+ const lastThreeCards = cardValues.slice(-3) as [number, number, number];
237
+
238
+ // this is done to account for ace being 1 or 14
239
+ const isStraight =
240
+ (firstThreeCards[0] + 1 === firstThreeCards[1] && firstThreeCards[1] + 1 === firstThreeCards[2]) ||
241
+ (lastThreeCards[0] + 1 === lastThreeCards[1] && lastThreeCards[1] + 1 === lastThreeCards[2]);
242
+ const threeOfAKind = dealerCardDetails.value === playerCard0.value && playerCard0.value === playerCard1.value;
243
+
244
+ if (dealerUpCard === playerCards[0] && playerCards[0] === playerCards[1]) {
245
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.SUITED_TRIPS;
246
+ } else if (isFlush && isStraight) {
247
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.STRAIGHT_FLUSH;
248
+ } else if (threeOfAKind) {
249
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.THREE_OF_A_KIND;
250
+ } else if (isStraight) {
251
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.STRAIGHT;
252
+ } else if (isFlush) {
253
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.FLUSH;
254
+ }
255
+
256
+ return results;
257
+ }
258
+
259
+ static calcHandValue(cards: number[]): number {
260
+ // taking Ace as 11 here
261
+ const cardValueMap: Record<CardValue, number> = {
262
+ '2': 2,
263
+ '3': 3,
264
+ '4': 4,
265
+ '5': 5,
266
+ '6': 6,
267
+ '7': 7,
268
+ '8': 8,
269
+ '9': 9,
270
+ '10': 10,
271
+ A: 11,
272
+ J: 10,
273
+ Q: 10,
274
+ K: 10,
275
+ };
276
+
277
+ let aceCount = CardGames.getCardDetailsArr(cards).filter((card) => card.value === 'A').length;
278
+ let cardSum = CardGames.getCardDetailsArr(cards).reduce((acc, cur) => acc + cardValueMap[cur.value], 0);
279
+
280
+ if (aceCount === 0) {
281
+ return cardSum;
282
+ }
283
+
284
+ // if there are aces, we need to consider the value of aces. If the sum is > 21, we need to consider the value of aces as 1
285
+ // Has aces and sum > 21
286
+ while (aceCount > 0 && cardSum > 21) {
287
+ cardSum -= 10;
288
+ aceCount--;
289
+ }
290
+
291
+ return cardSum;
292
+ }
293
+
294
+ static canBuyInsurance(dealerCards: number[]): boolean {
295
+ // player can only buy insurance if dealer's first card(upcard) is an ace
296
+ return CardGames.getCardDetails(dealerCards[0]).value === 'A';
297
+ }
298
+
299
+ // Dealer does NOT hit on soft 17 (for our implementation)
300
+ static shouldDealerHit(dealerCards: number[]): boolean {
301
+ return this.calcHandValue(dealerCards) < 17;
302
+ }
303
+
304
+ static hasBusted(cards: number[]): boolean {
305
+ return this.calcHandValue(cards) > 21;
306
+ }
307
+
308
+ static canSplit(mainHand: number[]): boolean {
309
+ if (mainHand.length !== 2) {
310
+ return false;
311
+ }
312
+ const cardActions = CardGames.getCardDetailsArr(mainHand);
313
+ return cardActions[0].value === cardActions[1].value;
314
+ }
315
+
316
+ static canDoubleDown(playerCards: number[]): boolean {
317
+ return playerCards.length === 2;
318
+ }
319
+
320
+ static isBlackjack(cards: number[]): boolean {
321
+ if (cards.length !== 2) {
322
+ return false;
323
+ }
324
+
325
+ return this.calcHandValue(cards) === 21;
326
+ }
327
+
328
+ /**
329
+ *
330
+ * Called after bet ends. Players can still win insurance or sidebets even if they lose the main bet
331
+ * @returns total payout amount
332
+ */
333
+ static getGameOutcome(data: SettleBetVars): { payout: BigNumber; multiplier: BigNumber; mainHandOutcome: HandOutcome; splitHandOutcome: HandOutcome } {
334
+ // calculate main bet payout for main and split hand
335
+ const mainHandBetAmount = data.mainHandDoubleDown ? data.mainBetAmount.times(2) : data.mainBetAmount;
336
+ let splitHandBetAmount = new BigNumber(0);
337
+ if (data.hasSplit) {
338
+ splitHandBetAmount = data.splitHandDoubleDown ? data.mainBetAmount.times(2) : data.mainBetAmount;
339
+ }
340
+
341
+ // calculate sidebet payouts
342
+ let perfectPairPayoutAmt = new BigNumber(0);
343
+ let twentyOnePlusThreePayoutAmt = new BigNumber(0);
344
+
345
+ const { mainHandOutcome, payout: mainBetPayout, splitHandOutcome } = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount });
346
+
347
+ const playerHand: [number, number] = data.hasSplit ? [data.mainPlayerHand[0], data.splitPlayerHand[0]] : [data.mainPlayerHand[0], data.mainPlayerHand[1]];
348
+
349
+ const { perfectPair, twentyOnePlusThree } = Blackjack.calcSideBetWins(data.dealerHand[0], playerHand);
350
+
351
+ if (perfectPairPayout[perfectPair] && data.perfectPairAmount.gt(0)) {
352
+ perfectPairPayoutAmt = data.perfectPairAmount.times(perfectPairPayout[perfectPair]);
353
+ }
354
+
355
+ if (twentyOnePlusThreePayout[twentyOnePlusThree] && data.twentyOnePlusThreeAmount.gt(0)) {
356
+ twentyOnePlusThreePayoutAmt = data.twentyOnePlusThreeAmount.times(twentyOnePlusThreePayout[twentyOnePlusThree]);
357
+ }
358
+
359
+ const payout = mainBetPayout.plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
360
+ const insuranceCost = data.boughtInsurance ? data.mainBetAmount.dividedBy(2) : new BigNumber(0); // insurance costs half of the main bet
361
+ const totalBetAmount = mainHandBetAmount.plus(splitHandBetAmount).plus(data.perfectPairAmount).plus(data.twentyOnePlusThreeAmount).plus(insuranceCost);
362
+
363
+ let multiplier = payout.dividedBy(totalBetAmount);
364
+
365
+ // when the user bets $0, assume an initial bet of $1 to calculate the multiplier
366
+ if (totalBetAmount.isZero()) {
367
+ const mainHandBetAmount = data.mainHandDoubleDown ? new BigNumber(2) : new BigNumber(1);
368
+ let splitHandBetAmount = new BigNumber(0);
369
+ if (data.hasSplit) {
370
+ splitHandBetAmount = data.splitHandDoubleDown ? new BigNumber(2) : new BigNumber(1);
371
+ }
372
+
373
+ const insuranceCost = data.boughtInsurance ? new BigNumber(0.5) : new BigNumber(0); // insurance costs half of the main bet
374
+ const totalBetAmount = mainHandBetAmount.plus(splitHandBetAmount).plus(insuranceCost);
375
+
376
+ multiplier = Blackjack.getMainBetPayout({ ...data, mainHandBetAmount, splitHandBetAmount }).payout.dividedBy(totalBetAmount);
377
+ }
378
+
379
+ return { payout, multiplier, mainHandOutcome, splitHandOutcome };
380
+ }
381
+
382
+ private static getMainBetPayout({
383
+ mainPlayerHand,
384
+ splitPlayerHand,
385
+ dealerHand,
386
+ mainHandBetAmount,
387
+ splitHandBetAmount,
388
+ boughtInsurance,
389
+ hasSplit,
390
+ }: MainBetPayoutVars) {
391
+ const { multiplier: mainHandMultiplier, outcome: mainHandOutcome } = Blackjack.getHandOutcome(dealerHand, mainPlayerHand, hasSplit);
392
+ const mainHandPayout = mainHandMultiplier.times(mainHandBetAmount);
393
+
394
+ const { multiplier: splitHandMultiplier, outcome: splitHandOutcome } = Blackjack.getHandOutcome(dealerHand, splitPlayerHand, hasSplit);
395
+
396
+ const splitHandPayout = splitPlayerHand.length > 0 ? splitHandMultiplier.times(splitHandBetAmount) : new BigNumber(0);
397
+
398
+ // calculate insurance payout (insurance payout is 3:1)
399
+ // insurance won't be claimed if double down occurs, so double downs won't affect insurance payout even though it doubles the mainHandBetAmount
400
+ const insurancePayout = boughtInsurance && Blackjack.isBlackjack(dealerHand) ? mainHandBetAmount.dividedBy(2).times(3) : new BigNumber(0);
401
+
402
+ return {
403
+ mainHandOutcome,
404
+ splitHandOutcome: hasSplit ? splitHandOutcome : HandOutcome.NONE,
405
+ payout: mainHandPayout.plus(splitHandPayout).plus(insurancePayout),
406
+ };
407
+ }
408
+ }
@@ -0,0 +1,94 @@
1
+ import { createHmac } from 'crypto';
2
+ import { CardGames, Suits } from './cardGames';
3
+
4
+ describe('Card Games', () => {
5
+ it('be able to provably generate result', () => {
6
+ const gameSeeds = [
7
+ {
8
+ serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
9
+ clientSeed: 'pf4jl5q97q',
10
+ nonce: '2',
11
+ results: [
12
+ 0, 28, 45, 20, 35, 44, 40, 7, 10, 49, 3, 9, 7, 36, 50, 36, 43, 34, 17, 39, 43, 14, 43, 4, 20, 23, 15, 8, 15, 49, 40, 16, 25, 42, 27, 44, 17, 27, 36,
13
+ 14, 33, 14, 29, 21, 13, 13, 11, 40, 46, 25, 38, 20,
14
+ ],
15
+ },
16
+ {
17
+ serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
18
+ clientSeed: 'pf4jl5q97q',
19
+ nonce: '3',
20
+ results: [
21
+ 17, 48, 16, 47, 3, 32, 48, 7, 33, 43, 23, 11, 7, 38, 34, 49, 47, 51, 44, 9, 21, 19, 49, 22, 9, 45, 34, 13, 35, 12, 30, 1, 38, 42, 19, 2, 12, 50, 21,
22
+ 9, 40, 26, 27, 29, 34, 43, 11, 42, 5, 1, 34, 3,
23
+ ],
24
+ },
25
+ {
26
+ serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
27
+ clientSeed: 'pf4jl5q97q',
28
+ nonce: '4',
29
+ results: [
30
+ 4, 37, 25, 49, 8, 4, 47, 48, 12, 25, 41, 17, 48, 24, 49, 14, 33, 48, 39, 42, 21, 32, 46, 21, 1, 9, 19, 44, 3, 29, 23, 11, 24, 35, 51, 38, 10, 44, 15,
31
+ 47, 49, 21, 22, 15, 16, 12, 9, 6, 2, 49, 21, 26,
32
+ ],
33
+ },
34
+ {
35
+ serverSeed: '652fb02cbce322c4232a9032d905ca580c77ed9cf4850ba6ce9990527d37949f',
36
+ clientSeed: 'l5sgv5cutr',
37
+ nonce: '2476',
38
+ results: [
39
+ 14, 39, 3, 13, 28, 19, 20, 6, 30, 25, 22, 34, 35, 11, 2, 39, 11, 4, 23, 44, 36, 12, 20, 10, 20, 45, 2, 48, 21, 42, 13, 42, 41, 51, 2, 15, 33, 49, 32,
40
+ 0, 41, 21, 12, 31, 39, 6, 16, 51, 34, 20, 48, 16,
41
+ ],
42
+ },
43
+ ];
44
+ gameSeeds.forEach((e) => {
45
+ const resultHex = generateDigestHex(e);
46
+ const results = CardGames.getResults(resultHex);
47
+ expect(results.slice(0, 52)).toEqual(e.results);
48
+ });
49
+ });
50
+
51
+ it('can give the right card details', () => {
52
+ expect(CardGames.getCardDetails(48).suit).toEqual(Suits.DIAMONDS);
53
+ expect(CardGames.getCardDetails(48).value).toEqual('A');
54
+ expect(CardGames.getCardDetails(49).suit).toEqual(Suits.HEARTS);
55
+ expect(CardGames.getCardDetails(49).value).toEqual('A');
56
+ expect(CardGames.getCardDetails(50).suit).toEqual(Suits.SPADES);
57
+ expect(CardGames.getCardDetails(50).value).toEqual('A');
58
+ expect(CardGames.getCardDetails(51).suit).toEqual(Suits.CLUBS);
59
+ expect(CardGames.getCardDetails(51).value).toEqual('A');
60
+
61
+ expect(CardGames.getCardDetails(47).value).toEqual('K');
62
+ expect(CardGames.getCardDetails(43).value).toEqual('Q');
63
+ expect(CardGames.getCardDetails(39).value).toEqual('J');
64
+ expect(CardGames.getCardDetails(27).value).toEqual('8');
65
+ });
66
+
67
+ it('can give the right card index', () => {
68
+ const zeroToFiftyOne = Array.from(Array(52).keys());
69
+ const cardDetails = zeroToFiftyOne.map((val) => CardGames.getCardDetails(val));
70
+ const indexes = cardDetails.map((val) => CardGames.getCardIndexFromDetails(val));
71
+ expect(indexes).toEqual(expect.arrayContaining(zeroToFiftyOne));
72
+ expect(zeroToFiftyOne).toEqual(expect.arrayContaining(indexes));
73
+ });
74
+ });
75
+
76
+ function generateDigestHex({
77
+ serverSeed,
78
+ clientSeed,
79
+ nonce,
80
+ rounds = 7,
81
+ }: {
82
+ serverSeed: string;
83
+ clientSeed: string;
84
+ nonce: string;
85
+ rounds?: number;
86
+ }): string[] {
87
+ const results = Array.from(Array(rounds).keys()).map((round) => {
88
+ const hmac = createHmac('sha256', serverSeed);
89
+ hmac.update(`${clientSeed}:${nonce}:${round}`);
90
+ return hmac.digest('hex');
91
+ });
92
+
93
+ return results;
94
+ }
@@ -0,0 +1,162 @@
1
+ import BigNumber from 'bignumber.js';
2
+ import { hexToBytes } from '../utils';
3
+
4
+ const BYTE_TOTAL = new BigNumber(256);
5
+
6
+ const CARD_MAP: Record<string, 'A' | 'J' | 'Q' | 'K'> = {
7
+ '1': 'A',
8
+ '11': 'J',
9
+ '12': 'Q',
10
+ '13': 'K',
11
+ };
12
+
13
+ export enum Suits {
14
+ DIAMONDS = 'DIAMONDS',
15
+ HEARTS = 'HEARTS',
16
+ SPADES = 'SPADES',
17
+ CLUBS = 'CLUBS',
18
+ }
19
+
20
+ export const suitsArr = [Suits.DIAMONDS, Suits.HEARTS, Suits.SPADES, Suits.CLUBS];
21
+
22
+ export type CardValue = '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '10' | 'J' | 'Q' | 'K' | 'A';
23
+
24
+ export interface CardDetail {
25
+ suit: Suits;
26
+ value: CardValue;
27
+ }
28
+
29
+ // contains generic card game util functions
30
+ export class CardGames {
31
+ static toComparableNo(result: number) {
32
+ const comparableNo = Math.trunc(result / 4) + 2;
33
+
34
+ if (comparableNo === 14) {
35
+ return 1;
36
+ } else {
37
+ return comparableNo;
38
+ }
39
+ }
40
+
41
+ static getCardDetails(cardNum: number): CardDetail {
42
+ if (cardNum > 51 || cardNum < 0 || !Number.isInteger(cardNum)) {
43
+ throw 'out of bounds';
44
+ }
45
+ const suit = suitsArr[cardNum % 4];
46
+ let value = this.toComparableNo(cardNum).toString();
47
+
48
+ if (CARD_MAP[value]) {
49
+ value = CARD_MAP[value];
50
+ }
51
+
52
+ return { suit, value: value as CardValue };
53
+ }
54
+
55
+ static getCardDetailsArr(cardNums: number[]): CardDetail[] {
56
+ return cardNums.map((cardNum) => this.getCardDetails(cardNum));
57
+ }
58
+
59
+ static getResult(hexStr: string, cardNo: number): number {
60
+ const bytePosition = cardNo % 8;
61
+ const bytes = hexToBytes(hexStr);
62
+ let result = new BigNumber(0);
63
+ for (let j = 0; j < 4; j++) {
64
+ const value = bytes[4 * bytePosition + j];
65
+ result = result.plus(new BigNumber(value).dividedBy(BYTE_TOTAL.pow(j + 1)));
66
+ }
67
+ return result.multipliedBy(52).integerValue(BigNumber.ROUND_FLOOR).toNumber();
68
+ }
69
+
70
+ static getResults(hexStr: string[]): number[] {
71
+ const results: number[] = [];
72
+ for (let i = 0; i < hexStr.length * 8; i++) {
73
+ const hexNo = Math.trunc(i / 8);
74
+ results.push(this.getResult(hexStr[hexNo], i));
75
+ }
76
+ return results;
77
+ }
78
+
79
+ // TODO: Test this
80
+ static getCardIndexFromDetails(card: CardDetail): number {
81
+ const suitIncrement = suitsArr.indexOf(card.suit);
82
+
83
+ if (card.value === 'J') {
84
+ return 9 * 4 + suitIncrement;
85
+ } else if (card.value === 'Q') {
86
+ return 10 * 4 + suitIncrement;
87
+ } else if (card.value === 'K') {
88
+ return 11 * 4 + suitIncrement;
89
+ } else if (card.value === 'A') {
90
+ return 12 * 4 + suitIncrement;
91
+ } else {
92
+ return (Number(card.value) - 2) * 4 + suitIncrement;
93
+ }
94
+ }
95
+ }
96
+
97
+ // Index of 0 to 51 : ♦2 to ♣A
98
+ // [
99
+ // ♦2, ♥2, ♠2, ♣2, ♦3, ♥3, ♠3, ♣3, ♦4, ♥4,
100
+ // ♠4, ♣4, ♦5, ♥5, ♠5, ♣5, ♦6, ♥6, ♠6, ♣6,
101
+ // ♦7, ♥7, ♠7, ♣7, ♦8, ♥8, ♠8, ♣8, ♦9, ♥9,
102
+ // ♠9, ♣9, ♦10, ♥10, ♠10, ♣10, ♦J, ♥J, ♠J,
103
+ // ♣J, ♦Q, ♥Q, ♠Q, ♣Q, ♦K, ♥K, ♠K, ♣K, ♦A,
104
+ // ♥A, ♠A, ♣A
105
+ // ]
106
+
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
+ */