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.
- package/lib/games/blackjack.d.ts +58 -0
- package/lib/games/blackjack.js +253 -0
- package/lib/games/blackjack.js.map +1 -0
- package/lib/games/cardGames.d.ts +20 -0
- package/lib/games/cardGames.js +86 -0
- package/lib/games/cardGames.js.map +1 -0
- package/lib/games/hilo.d.ts +0 -4
- package/lib/games/hilo.js +4 -49
- package/lib/games/hilo.js.map +1 -1
- package/lib/index.d.ts +3 -1
- package/lib/index.js +13 -2
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +4 -1
- package/lib/utils/index.js.map +1 -1
- package/package.json +2 -2
- package/src/games/blackjack.spec.ts +1074 -0
- package/src/games/blackjack.ts +352 -0
- package/src/games/cardGames.spec.ts +94 -0
- package/src/games/cardGames.ts +107 -0
- package/src/games/hilo.spec.ts +1 -84
- package/src/games/hilo.ts +6 -68
- package/src/index.ts +11 -1
- package/src/utils/index.ts +1 -0
|
@@ -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
|
package/src/games/hilo.spec.ts
CHANGED
|
@@ -1,54 +1,7 @@
|
|
|
1
|
-
import { createHmac } from 'crypto';
|
|
2
1
|
import BigNumber from 'bignumber.js';
|
|
3
|
-
import { Hilo, HiloGuess
|
|
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
|
-
}
|