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