shufflecom-calculations 1.2.0 → 1.2.1

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,352 @@
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 PerfectPairType {
15
+ PERFECT_PAIR = 'PERFECT_PAIR',
16
+ COLORED_PAIR = 'COLORED_PAIR',
17
+ MIXED_PAIR = 'MIXED_PAIR',
18
+ }
19
+ export enum TwentyOnePlusThreeType {
20
+ SUITED_TRIPS = 'SUITED_TRIPS',
21
+ STRAIGHT_FLUSH = 'STRAIGHT_FLUSH',
22
+ STRAIGHT = 'STRAIGHT',
23
+ THREE_OF_A_KIND = 'THREE_OF_A_KIND',
24
+ FLUSH = 'FLUSH',
25
+ }
26
+
27
+ export enum InsuranceStatus {
28
+ INELIGIBLE = 'INELIGIBLE',
29
+ BOUGHT_PAYS_OUT = 'BOUGHT_PAYS_OUT',
30
+ BOUGHT_DOES_NOT_PAY_OUT = 'BOUGHT_DOES_NOT_PAY_OUT',
31
+ REJECTED = 'REJECTED',
32
+ // if in eligible stage, user must choose to buy or not buy
33
+ ELIGIBLE = 'ELIGIBLE',
34
+ }
35
+
36
+ export interface SettleBetVars {
37
+ mainPlayerHand: number[];
38
+ splitPlayerHand: number[];
39
+ dealerHand: number[];
40
+ mainHandBetAmount: BigNumber;
41
+ splitHandBetAmount: BigNumber;
42
+ twentyOnePlusThreeAmount: BigNumber;
43
+ perfectPairAmount: BigNumber;
44
+ boughtInsurance: boolean;
45
+ hasSplit: boolean;
46
+ }
47
+
48
+ // Since the values are payout and not profit, it's the profit multiple + 1
49
+ export const perfectPairPayout: Record<PerfectPairType, BigNumber> = {
50
+ [PerfectPairType.PERFECT_PAIR]: new BigNumber(26),
51
+ [PerfectPairType.COLORED_PAIR]: new BigNumber(13),
52
+ [PerfectPairType.MIXED_PAIR]: new BigNumber(7),
53
+ };
54
+
55
+ export const twentyOnePlusThreePayout: Record<TwentyOnePlusThreeType, BigNumber> = {
56
+ [TwentyOnePlusThreeType.SUITED_TRIPS]: new BigNumber(101),
57
+ [TwentyOnePlusThreeType.STRAIGHT_FLUSH]: new BigNumber(41),
58
+ [TwentyOnePlusThreeType.THREE_OF_A_KIND]: new BigNumber(31),
59
+ [TwentyOnePlusThreeType.STRAIGHT]: new BigNumber(11),
60
+ [TwentyOnePlusThreeType.FLUSH]: new BigNumber(6),
61
+ };
62
+
63
+ enum SuitColor {
64
+ RED = 'RED',
65
+ BLACK = 'BLACK',
66
+ }
67
+ const suitColorMap: Record<Suits, SuitColor> = {
68
+ [Suits.DIAMONDS]: SuitColor.RED,
69
+ [Suits.HEARTS]: SuitColor.RED,
70
+ [Suits.SPADES]: SuitColor.BLACK,
71
+ [Suits.CLUBS]: SuitColor.BLACK,
72
+ };
73
+
74
+ export class Blackjack {
75
+ static availableNextActions(
76
+ currentPlayerHand: number[],
77
+ dealerCards: number[],
78
+ insuranceStatus: InsuranceStatus,
79
+ atMainHandAndHasNotSplit: boolean,
80
+ ): BlackjackAction[] {
81
+ // User has to choose either one before they can continue
82
+ if (this.canBuyInsurance(dealerCards) && insuranceStatus === InsuranceStatus.ELIGIBLE) {
83
+ return [BlackjackAction.BUY_INSURANCE, BlackjackAction.REJECT_INSURANCE];
84
+ }
85
+
86
+ if (this.calcHandValue(currentPlayerHand) >= 21) {
87
+ return [];
88
+ }
89
+
90
+ const actions: BlackjackAction[] = [BlackjackAction.HIT, BlackjackAction.STAND];
91
+
92
+ if (this.canDoubleDown(currentPlayerHand)) {
93
+ actions.push(BlackjackAction.DOUBLE_DOWN);
94
+ }
95
+
96
+ if (this.canSplit(currentPlayerHand) && atMainHandAndHasNotSplit) {
97
+ actions.push(BlackjackAction.SPLIT);
98
+ }
99
+
100
+ return actions;
101
+ }
102
+
103
+ static playingMainHand(
104
+ mainPlayerCards: number[],
105
+ splitPlayerCards: number[],
106
+ mainPlayerActions: BlackjackAction[],
107
+ splitPlayerActions: BlackjackAction[],
108
+ ): boolean {
109
+ // 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
+ if (
111
+ this.calcHandValue(mainPlayerCards) < 21 &&
112
+ !mainPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) &&
113
+ !mainPlayerActions.includes(BlackjackAction.STAND)
114
+ ) {
115
+ return true;
116
+ }
117
+
118
+ // on split hand if player has split and split hand is not yet over
119
+ if (
120
+ splitPlayerCards.length > 0 &&
121
+ this.calcHandValue(splitPlayerCards) < 21 &&
122
+ !splitPlayerActions.includes(BlackjackAction.DOUBLE_DOWN) &&
123
+ !splitPlayerActions.includes(BlackjackAction.STAND)
124
+ ) {
125
+ return false;
126
+ }
127
+
128
+ // player blackjack and dealer upcard A, so user is choosing whether to buy insurance in this case, and will end immediately after this action
129
+ if (mainPlayerCards.length === 2 && this.calcHandValue(mainPlayerCards) === 21 && splitPlayerCards.length === 0) {
130
+ return true;
131
+ }
132
+
133
+ // main hand busted/ended and no split hand
134
+ // OR main hand busted and split hand busted/ended
135
+ throw new Error('No available action');
136
+ }
137
+
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));
141
+
142
+ if (playerBlackjack && !dealerBlackjack && !hasSplit) {
143
+ return new BigNumber(2.5);
144
+ }
145
+
146
+ if (dealerBlackjack && !playerBlackjack) {
147
+ return new BigNumber(0);
148
+ }
149
+
150
+ if (dealerBlackjack && playerBlackjack) {
151
+ return new BigNumber(1);
152
+ }
153
+
154
+ // If it reaches here, neither player nor dealer has blackjack
155
+ // if player busted, dealer auto wins
156
+ if (this.hasBusted(playerCards)) {
157
+ return new BigNumber(0);
158
+ }
159
+
160
+ if (this.hasBusted(dealerCards)) {
161
+ return new BigNumber(2);
162
+ }
163
+
164
+ // Both did not bust, so compare the sum of cards
165
+ const playerSum = this.calcHandValue(playerCards);
166
+ const dealerSum = this.calcHandValue(dealerCards);
167
+ if (playerSum > dealerSum) {
168
+ return new BigNumber(2);
169
+ } else if (playerSum < dealerSum) {
170
+ return new BigNumber(0);
171
+ } else {
172
+ return new BigNumber(1);
173
+ }
174
+ }
175
+
176
+ static calcSideBetWins(dealerUpCard: number, playerCards: number[]): { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } {
177
+ const results: { perfectPair?: PerfectPairType; twentyOnePlusThree?: TwentyOnePlusThreeType } = {};
178
+ const dealerCardDetails = CardGames.getCardDetails(dealerUpCard);
179
+ const playerCard0 = CardGames.getCardDetails(playerCards[0]);
180
+ const playerCard1 = CardGames.getCardDetails(playerCards[1]);
181
+
182
+ // Perfect pairs
183
+ if (playerCard0.value === playerCard1.value) {
184
+ const cardSuits = [playerCard0.suit, playerCard1.suit] as const;
185
+
186
+ if (playerCards[0] === playerCards[1]) {
187
+ results.perfectPair = PerfectPairType.PERFECT_PAIR;
188
+ } else if (suitColorMap[cardSuits[0]] === suitColorMap[cardSuits[1]]) {
189
+ results.perfectPair = PerfectPairType.COLORED_PAIR;
190
+ } else {
191
+ results.perfectPair = PerfectPairType.MIXED_PAIR;
192
+ }
193
+ }
194
+
195
+ // 21+3
196
+ const cardValueMap: Record<CardValue, number[]> = {
197
+ '2': [2],
198
+ '3': [3],
199
+ '4': [4],
200
+ '5': [5],
201
+ '6': [6],
202
+ '7': [7],
203
+ '8': [8],
204
+ '9': [9],
205
+ '10': [10],
206
+ J: [11],
207
+ Q: [12],
208
+ K: [13],
209
+ A: [14, 1],
210
+ };
211
+
212
+ const cardValues = [...cardValueMap[playerCard0.value], ...cardValueMap[playerCard1.value], ...cardValueMap[dealerCardDetails.value]].sort((a, b) => a - b);
213
+
214
+ const isFlush = dealerCardDetails.suit === playerCard0.suit && playerCard0.suit === playerCard1.suit;
215
+ const firstThreeCards = cardValues.slice(0, 3) as [number, number, number];
216
+ const lastThreeCards = cardValues.slice(-3) as [number, number, number];
217
+
218
+ // this is done to account for ace being 1 or 14
219
+ const isStraight =
220
+ (firstThreeCards[0] + 1 === firstThreeCards[1] && firstThreeCards[1] + 1 === firstThreeCards[2]) ||
221
+ (lastThreeCards[0] + 1 === lastThreeCards[1] && lastThreeCards[1] + 1 === lastThreeCards[2]);
222
+ const threeOfAKind = dealerCardDetails.value === playerCard0.value && playerCard0.value === playerCard1.value;
223
+
224
+ if (dealerUpCard === playerCards[0] && playerCards[0] === playerCards[1]) {
225
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.SUITED_TRIPS;
226
+ } else if (isFlush && isStraight) {
227
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.STRAIGHT_FLUSH;
228
+ } else if (threeOfAKind) {
229
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.THREE_OF_A_KIND;
230
+ } else if (isStraight) {
231
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.STRAIGHT;
232
+ } else if (isFlush) {
233
+ results.twentyOnePlusThree = TwentyOnePlusThreeType.FLUSH;
234
+ }
235
+
236
+ return results;
237
+ }
238
+
239
+ static calcHandValue(cards: number[]): number {
240
+ // taking Ace as 11 here
241
+ const cardValueMap: Record<CardValue, number> = {
242
+ '2': 2,
243
+ '3': 3,
244
+ '4': 4,
245
+ '5': 5,
246
+ '6': 6,
247
+ '7': 7,
248
+ '8': 8,
249
+ '9': 9,
250
+ '10': 10,
251
+ A: 11,
252
+ J: 10,
253
+ Q: 10,
254
+ K: 10,
255
+ };
256
+
257
+ let aceCount = CardGames.getCardDetailsArr(cards).filter((card) => card.value === 'A').length;
258
+ let cardSum = CardGames.getCardDetailsArr(cards).reduce((acc, cur) => acc + cardValueMap[cur.value], 0);
259
+
260
+ if (aceCount === 0) {
261
+ return cardSum;
262
+ }
263
+
264
+ // 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
265
+ // Has aces and sum > 21
266
+ while (aceCount > 0 && cardSum > 21) {
267
+ cardSum -= 10;
268
+ aceCount--;
269
+ }
270
+
271
+ return cardSum;
272
+ }
273
+
274
+ static canBuyInsurance(dealerCards: number[]): boolean {
275
+ // player can only buy insurance if dealer's first card(upcard) is an ace
276
+ return CardGames.getCardDetails(dealerCards[0]).value === 'A';
277
+ }
278
+
279
+ // Dealer does NOT hit on soft 17 (for our implementation)
280
+ static shouldDealerHit(dealerCards: number[]): boolean {
281
+ return this.calcHandValue(dealerCards) < 17;
282
+ }
283
+
284
+ static hasBusted(cards: number[]): boolean {
285
+ return this.calcHandValue(cards) > 21;
286
+ }
287
+
288
+ static canSplit(mainHand: number[]): boolean {
289
+ if (mainHand.length !== 2) {
290
+ return false;
291
+ }
292
+ const cardActions = CardGames.getCardDetailsArr(mainHand);
293
+ return cardActions[0].value === cardActions[1].value;
294
+ }
295
+
296
+ static canDoubleDown(playerCards: number[]): boolean {
297
+ return playerCards.length === 2;
298
+ }
299
+
300
+ static isBlackjack(cards: number[]): boolean {
301
+ if (cards.length !== 2) {
302
+ return false;
303
+ }
304
+
305
+ return this.calcHandValue(cards) === 21;
306
+ }
307
+
308
+ /**
309
+ *
310
+ * Called after bet ends. Players can still win insurance or sidebets even if they lose the main bet
311
+ * @returns total payout amount
312
+ */
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 {
325
+ // 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);
333
+
334
+ // calculate sidebet payouts
335
+ let perfectPairPayoutAmt = new BigNumber(0);
336
+ let twentyOnePlusThreePayoutAmt = new BigNumber(0);
337
+
338
+ const playerHand: [number, number] = hasSplit ? [mainPlayerHand[0], splitPlayerHand[0]] : [mainPlayerHand[0], mainPlayerHand[1]];
339
+
340
+ const { perfectPair, twentyOnePlusThree } = Blackjack.calcSideBetWins(dealerHand[0], playerHand);
341
+
342
+ if (perfectPairPayout[perfectPair] && perfectPairAmount.gt(0)) {
343
+ perfectPairPayoutAmt = perfectPairAmount.times(perfectPairPayout[perfectPair]);
344
+ }
345
+
346
+ if (twentyOnePlusThreePayout[twentyOnePlusThree] && twentyOnePlusThreeAmount.gt(0)) {
347
+ twentyOnePlusThreePayoutAmt = twentyOnePlusThreeAmount.times(twentyOnePlusThreePayout[twentyOnePlusThree]);
348
+ }
349
+
350
+ return mainBetPayout.plus(splitBetPayout).plus(insurancePayout).plus(perfectPairPayoutAmt).plus(twentyOnePlusThreePayoutAmt);
351
+ }
352
+ }
@@ -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,107 @@
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
@@ -1,54 +1,7 @@
1
- import { createHmac } from 'crypto';
2
1
  import BigNumber from 'bignumber.js';
3
- import { Hilo, HiloGuess, Suits } from './hilo';
2
+ import { Hilo, HiloGuess } from './hilo';
4
3
 
5
4
  describe('HiLo', () => {
6
- it('be able to provably generate result', () => {
7
- const gameSeeds = [
8
- {
9
- serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
10
- clientSeed: 'pf4jl5q97q',
11
- nonce: '2',
12
- results: [
13
- 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,
14
- 14, 33, 14, 29, 21, 13, 13, 11, 40, 46, 25, 38, 20,
15
- ],
16
- },
17
- {
18
- serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
19
- clientSeed: 'pf4jl5q97q',
20
- nonce: '3',
21
- results: [
22
- 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,
23
- 9, 40, 26, 27, 29, 34, 43, 11, 42, 5, 1, 34, 3,
24
- ],
25
- },
26
- {
27
- serverSeed: 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea',
28
- clientSeed: 'pf4jl5q97q',
29
- nonce: '4',
30
- results: [
31
- 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,
32
- 47, 49, 21, 22, 15, 16, 12, 9, 6, 2, 49, 21, 26,
33
- ],
34
- },
35
- {
36
- serverSeed: '652fb02cbce322c4232a9032d905ca580c77ed9cf4850ba6ce9990527d37949f',
37
- clientSeed: 'l5sgv5cutr',
38
- nonce: '2476',
39
- results: [
40
- 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,
41
- 0, 41, 21, 12, 31, 39, 6, 16, 51, 34, 20, 48, 16,
42
- ],
43
- },
44
- ];
45
- gameSeeds.forEach((e) => {
46
- const resultHex = generateDigestHex(e);
47
- const results = Hilo.getResults(resultHex);
48
- expect(results.slice(0, 52)).toEqual(e.results);
49
- });
50
- });
51
-
52
5
  it('can check that a guess is correct', () => {
53
6
  const correctAns = [
54
7
  // Ace
@@ -105,40 +58,4 @@ describe('HiLo', () => {
105
58
  expect(Hilo.calculateWinChanceBN(HiloGuess.SAME_OR_BELOW, 20).toString()).toEqual(new BigNumber(7).dividedBy(13).toString());
106
59
  expect(Hilo.calculateWinChanceBN(HiloGuess.SAME_OR_BELOW, 14).toString()).toEqual(new BigNumber(5).dividedBy(13).toString());
107
60
  });
108
-
109
- it('can give the right card details', () => {
110
- expect(Hilo.getCardDetails(48).suit).toEqual(Suits.DIAMONDS);
111
- expect(Hilo.getCardDetails(48).value).toEqual('A');
112
- expect(Hilo.getCardDetails(49).suit).toEqual(Suits.HEARTS);
113
- expect(Hilo.getCardDetails(49).value).toEqual('A');
114
- expect(Hilo.getCardDetails(50).suit).toEqual(Suits.SPADES);
115
- expect(Hilo.getCardDetails(50).value).toEqual('A');
116
- expect(Hilo.getCardDetails(51).suit).toEqual(Suits.CLUBS);
117
- expect(Hilo.getCardDetails(51).value).toEqual('A');
118
-
119
- expect(Hilo.getCardDetails(47).value).toEqual('K');
120
- expect(Hilo.getCardDetails(43).value).toEqual('Q');
121
- expect(Hilo.getCardDetails(39).value).toEqual('J');
122
- expect(Hilo.getCardDetails(27).value).toEqual('8');
123
- });
124
61
  });
125
-
126
- function generateDigestHex({
127
- serverSeed,
128
- clientSeed,
129
- nonce,
130
- rounds = 7,
131
- }: {
132
- serverSeed: string;
133
- clientSeed: string;
134
- nonce: string;
135
- rounds?: number;
136
- }): string[] {
137
- const results = Array.from(Array(rounds).keys()).map((round) => {
138
- const hmac = createHmac('sha256', serverSeed);
139
- hmac.update(`${clientSeed}:${nonce}:${round}`);
140
- return hmac.digest('hex');
141
- });
142
-
143
- return results;
144
- }