shufflecom-calculations 4.3.2 → 4.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shufflecom-calculations",
3
- "version": "4.3.2",
3
+ "version": "4.4.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": "267cf8cd0477307db6392d3485cf1557371b098c"
17
+ "gitHead": "12c4928ea14e1a42051cac1343a075debff13562"
18
18
  }
@@ -1,5 +1,6 @@
1
1
  import { createHmac } from 'crypto';
2
2
  import { Blitz } from './blitz';
3
+ import { CardGames } from './cardGames';
3
4
 
4
5
  const SERVER_SEED = 'ebd3ff712ee7ed3cabe3753146f95847b017f847758ed254c531ff0d45686cea';
5
6
  const CLIENT_SEED = 'pf4jl5q97q';
@@ -47,36 +48,20 @@ function generateDigestHex(nonce: string, rounds: number): string[] {
47
48
  });
48
49
  }
49
50
 
50
- describe('Blitz.getGameSeedRounds', () => {
51
- it('returns 1 round for picks 1–8', () => {
52
- expect(Blitz.getGameSeedRounds(5)).toBe(1);
53
- expect(Blitz.getGameSeedRounds(8)).toBe(1);
54
- });
55
-
56
- it('returns 2 rounds for picks 9–16', () => {
57
- expect(Blitz.getGameSeedRounds(9)).toBe(2);
58
- expect(Blitz.getGameSeedRounds(16)).toBe(2);
59
- });
60
-
61
- it('returns 4 rounds for 30 picks (max)', () => {
62
- expect(Blitz.getGameSeedRounds(30)).toBe(4);
63
- });
64
- });
65
-
66
51
  describe('Blitz.getGameOutcome', () => {
67
52
  it('returns the correct number of cards', () => {
68
- const hexStrings = generateDigestHex('1', Blitz.getGameSeedRounds(10));
53
+ const hexStrings = generateDigestHex('1', Blitz.getMaxUniqueCardsSeedRounds());
69
54
  const { cards } = Blitz.getGameOutcome(hexStrings, 10);
70
55
  expect(cards).toHaveLength(10);
71
56
 
72
- const hexStrings2 = generateDigestHex('2', Blitz.getGameSeedRounds(25));
57
+ const hexStrings2 = generateDigestHex('2', Blitz.getMaxUniqueCardsSeedRounds());
73
58
  const { cards: cards2 } = Blitz.getGameOutcome(hexStrings2, 25);
74
59
  expect(cards2).toHaveLength(25);
75
60
  });
76
61
 
77
62
  it('maxCount is 1 when all cards are unique', () => {
78
63
  // Force a known set of unique cards by checking the outcome
79
- const hexStrings = generateDigestHex('1', Blitz.getGameSeedRounds(5));
64
+ const hexStrings = generateDigestHex('1', Blitz.getMaxUniqueCardsSeedRounds());
80
65
  const { maxRepeatedCount, cards } = Blitz.getGameOutcome(hexStrings, 5);
81
66
  const uniqueCount = new Set(cards).size;
82
67
  if (uniqueCount === 5) {
@@ -87,15 +72,59 @@ describe('Blitz.getGameOutcome', () => {
87
72
  });
88
73
 
89
74
  it('maxCount reflects the highest duplicate count', () => {
90
- const hexStrings = generateDigestHex('5', Blitz.getGameSeedRounds(30));
75
+ const hexStrings = generateDigestHex('5', Blitz.getMaxUniqueCardsSeedRounds());
91
76
  const { maxRepeatedCount } = Blitz.getGameOutcome(hexStrings, 30);
92
77
 
93
78
  expect(maxRepeatedCount).toBe(2);
94
79
 
95
- const hexStrings2 = generateDigestHex('6', Blitz.getGameSeedRounds(30));
80
+ const hexStrings2 = generateDigestHex('6', Blitz.getMaxUniqueCardsSeedRounds());
96
81
  const { maxRepeatedCount: maxRepeatedCount2 } = Blitz.getGameOutcome(hexStrings2, 30);
97
82
  expect(maxRepeatedCount2).toBe(3);
98
83
  });
84
+
85
+ describe('maxUniqueCardsBeforeDuplicate', () => {
86
+ function bruteForceUniqueCountBeforeDuplicate(hexStrings: string[]): number {
87
+ const cards = CardGames.getResults(hexStrings);
88
+ const seen = new Set<number>();
89
+
90
+ for (const card of cards) {
91
+ if (seen.has(card)) return seen.size;
92
+ seen.add(card);
93
+ }
94
+
95
+ return seen.size;
96
+ }
97
+
98
+ it('matches a brute-force scan of all drawn cards for several nonces', () => {
99
+ for (const nonce of ['1', '2', '5', '6', '42']) {
100
+ const hexStrings = generateDigestHex(nonce, Blitz.getMaxUniqueCardsSeedRounds());
101
+ const { maxUniqueCardsBeforeDuplicate } = Blitz.getGameOutcome(hexStrings, 5);
102
+ expect(maxUniqueCardsBeforeDuplicate).toBe(bruteForceUniqueCountBeforeDuplicate(hexStrings));
103
+ }
104
+ });
105
+
106
+ it('never exceeds the number of distinct cards in a deck', () => {
107
+ for (const nonce of ['1', '2', '5', '6', '42']) {
108
+ const hexStrings = generateDigestHex(nonce, Blitz.getMaxUniqueCardsSeedRounds());
109
+ const { maxUniqueCardsBeforeDuplicate } = Blitz.getGameOutcome(hexStrings, 5);
110
+ expect(maxUniqueCardsBeforeDuplicate).toBeLessThanOrEqual(52);
111
+ }
112
+ });
113
+
114
+ it('is independent of picks, since it scans the full drawn card sequence', () => {
115
+ const hexStrings = generateDigestHex('1', Blitz.getMaxUniqueCardsSeedRounds());
116
+ const outcomeA = Blitz.getGameOutcome(hexStrings, 5);
117
+ const outcomeB = Blitz.getGameOutcome(hexStrings, 30);
118
+
119
+ expect(outcomeA.maxUniqueCardsBeforeDuplicate).toBe(outcomeB.maxUniqueCardsBeforeDuplicate);
120
+ });
121
+ });
122
+ });
123
+
124
+ describe('Blitz.getMaxUniqueCardsSeedRounds', () => {
125
+ it('returns enough rounds to guarantee a duplicate within a 52-card deck', () => {
126
+ expect(Blitz.getMaxUniqueCardsSeedRounds()).toBe(7);
127
+ });
99
128
  });
100
129
 
101
130
  describe('Blitz.isCleanSweepWin', () => {
@@ -8,36 +8,46 @@ export const BLITZ_MAX_PICKS = 36;
8
8
  export interface BlitzResult {
9
9
  maxRepeatedCount: number;
10
10
  cards: number[];
11
+ maxUniqueCardsBeforeDuplicate: number;
11
12
  }
12
13
 
13
14
  export class Blitz {
14
- static getGameSeedRounds(picks: number): number {
15
- return Math.ceil(picks / 8);
15
+ static getMaxUniqueCardsSeedRounds(): number {
16
+ return Math.ceil((CARDS_PER_DECK + 1) / 8);
16
17
  }
17
18
 
18
- private static drawCards(hexStrings: string[], picks: number): number[] {
19
- const cards = CardGames.getResults(hexStrings);
19
+ static getGameOutcome(hexStrings: string[], picks: number): BlitzResult {
20
+ const allCards = CardGames.getResults(hexStrings);
20
21
 
21
- if (cards.length < picks) {
22
+ if (allCards.length < picks) {
22
23
  throw new Error('Not enough cards drawn');
23
24
  }
24
25
 
25
- return cards.slice(0, picks);
26
- }
27
-
28
- static getGameOutcome(hexStrings: string[], picks: number): BlitzResult {
29
- const cards = this.drawCards(hexStrings, picks);
30
26
  const cardCounts: Record<number, number> = {};
31
27
  let maxRepeatedCount = 0;
28
+ let maxUniqueCardsBeforeDuplicate = 0;
29
+ let duplicateFound = false;
30
+
31
+ for (let i = 0; i < allCards.length; i++) {
32
+ const card = allCards[i];
33
+ const isNewCard = !(card in cardCounts);
34
+
35
+ cardCounts[card] = (cardCounts[card] || 0) + 1;
32
36
 
33
- for (const card of cards) {
34
- const count = (cardCounts[card] || 0) + 1;
35
- cardCounts[card] = count;
37
+ if (i < picks && cardCounts[card] > maxRepeatedCount) {
38
+ maxRepeatedCount = cardCounts[card];
39
+ }
36
40
 
37
- if (count > maxRepeatedCount) maxRepeatedCount = count;
41
+ if (!duplicateFound) {
42
+ if (isNewCard) {
43
+ maxUniqueCardsBeforeDuplicate++;
44
+ } else {
45
+ duplicateFound = true;
46
+ }
47
+ }
38
48
  }
39
49
 
40
- return { maxRepeatedCount, cards };
50
+ return { maxRepeatedCount, cards: allCards.slice(0, picks), maxUniqueCardsBeforeDuplicate };
41
51
  }
42
52
 
43
53
  static calculateWinChanceBN(picks: number): BigNumber {
@@ -6,7 +6,6 @@ import {
6
6
  SportsFixtureStatus,
7
7
  SportsMarketGroup,
8
8
  SportsMarketProvider,
9
- SportsMatchPhase,
10
9
  SportsMatchPhaseHomeAway,
11
10
  SportsMatchPhaseSummary,
12
11
  SportsMatchStatistics,
@@ -0,0 +1,20 @@
1
+ import { PrebuildBetFeaturedPrebuilds } from './prebuild-bet.types';
2
+ import { PopularSportsCompetitionsV2Info } from './sports-competition-fixture.types';
3
+ import { SportsContentFeaturedEvents, SportsContentFeaturedOutright } from './sports-content-fixture.types';
4
+ import { PaginatedSportsBoostedSelectionsInfo } from './sports-selection.types';
5
+ import { Nullable, SportsContentType } from './sports.types';
6
+
7
+ export class SportsContentWeight {
8
+ sportsContentType: SportsContentType;
9
+ weight: number;
10
+ }
11
+
12
+ export class SportsContentHomePage {
13
+ featuredEvents: Nullable<SportsContentFeaturedEvents>;
14
+ featuredOutright: Nullable<SportsContentFeaturedOutright>;
15
+ popularCompetitions: Nullable<PopularSportsCompetitionsV2Info>;
16
+ featuredPrebuilds: Nullable<PrebuildBetFeaturedPrebuilds>;
17
+ sportsCustomBoosts: Nullable<PaginatedSportsBoostedSelectionsInfo>;
18
+ /** Enabled content types with their weights, ordered by weight DESC. */
19
+ weights: SportsContentWeight[];
20
+ }
@@ -27,6 +27,7 @@ export type SportsMarketSelectionType = {
27
27
  name: string;
28
28
  isOutrightLike: boolean;
29
29
  shortName: string;
30
+ startTime: string;
30
31
  };
31
32
  competition: {
32
33
  id: string;
@@ -41,3 +42,12 @@ export type SportsMarketSelectionType = {
41
42
  };
42
43
  provider: SportsMarketProvider;
43
44
  };
45
+
46
+ export type SportsCustomBoostMarketSelectionType = SportsMarketSelectionType & {
47
+ boost: SportsMarketSelectionOddsBoost;
48
+ };
49
+
50
+ export class PaginatedSportsBoostedSelectionsInfo {
51
+ nodes: SportsCustomBoostMarketSelectionType[];
52
+ nextCursor: string | null;
53
+ }
@@ -1,5 +1,6 @@
1
1
  import BigNumber from 'bignumber.js';
2
2
  import { SportsMatchPhaseLabel } from './sports-match-state.types';
3
+ import { SportsCustomBoostMarketSelectionType } from './sports-selection.types';
3
4
 
4
5
  export enum SportsMarketProvider {
5
6
  SHUFFLE = 'SHUFFLE',
@@ -124,6 +125,20 @@ export enum SportsFixtureBannerType {
124
125
  SINGLE_ELIMINATION = 'SINGLE_ELIMINATION',
125
126
  }
126
127
 
128
+ /** SPORT_1..SPORT_6 are positional slots on the home page; each holds one popular sport's competitions. */
129
+ export enum SportsContentType {
130
+ HOME_FEATURED_EVENT = 'HOME_FEATURED_EVENT',
131
+ FEATURED_CUSTOM_BETS = 'FEATURED_CUSTOM_BETS',
132
+ FEATURED_OUTRIGHT = 'FEATURED_OUTRIGHT',
133
+ FEATURED_BOOSTS = 'FEATURED_BOOSTS',
134
+ SPORT_1 = 'SPORT_1',
135
+ SPORT_2 = 'SPORT_2',
136
+ SPORT_3 = 'SPORT_3',
137
+ SPORT_4 = 'SPORT_4',
138
+ SPORT_5 = 'SPORT_5',
139
+ SPORT_6 = 'SPORT_6',
140
+ }
141
+
127
142
  export enum SportsMarketSelectionTeam {
128
143
  HOME = 'HOME',
129
144
  AWAY = 'AWAY',
@@ -665,6 +680,7 @@ export class DisplayGroup extends BaseDisplayGroup {
665
680
  groupDisplayType: SportsGroupedSelectionsDisplayType;
666
681
  playerMarkets: PlayerMarket[] | null;
667
682
  groups: Partial<Record<SportsMarketGroup, number>> | null; // For SGM sub-groups
683
+ cmsDescriptionSlug: string | null;
668
684
  }
669
685
 
670
686
  export class SelectionGroup {
@@ -726,6 +742,7 @@ export class SgmSubGroup {
726
742
  export class SportsMarketInfo {
727
743
  odds: SportsNewMarketOddsUpdate[];
728
744
  display: DisplayGroup[];
745
+ customBoosts: SportsCustomBoostMarketSelectionType[];
729
746
  updatedAt: Nullable<Date>;
730
747
  sgmSubGroups: Nullable<SgmSubGroup[]>;
731
748
  }