shufflecom-calculations 4.1.2 → 4.1.4
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/floor-is-lava-rules.d.ts +19 -5
- package/lib/games/floor-is-lava-rules.js +18 -5
- package/lib/games/floor-is-lava-rules.js.map +1 -1
- package/lib/games/floor-is-lava.d.ts +8 -7
- package/lib/games/floor-is-lava.js +25 -19
- package/lib/games/floor-is-lava.js.map +1 -1
- package/lib/lottery/lottery.d.ts +1 -1
- package/lib/lottery/lottery.js +1 -1
- package/lib/sports/sports.types.d.ts +5 -7
- package/lib/sports/sports.types.js +4 -5
- package/lib/sports/sports.types.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/games/floor-is-lava-rules.spec.ts +30 -4
- package/src/games/floor-is-lava-rules.ts +35 -9
- package/src/games/floor-is-lava.spec.ts +73 -40
- package/src/games/floor-is-lava.ts +34 -19
- package/src/lottery/lottery.ts +1 -1
- package/src/sports/sports.types.ts +6 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shufflecom-calculations",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.4",
|
|
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": "1d3e41e1850ad39ea02cdec7a884a03472bf5fa7"
|
|
18
18
|
}
|
|
@@ -4,17 +4,23 @@ import { FixedSequenceRng } from './fixed-rng';
|
|
|
4
4
|
import {
|
|
5
5
|
BOARD_SIZE,
|
|
6
6
|
DIFFICULTY_DROP_MAP,
|
|
7
|
+
FloorIsLavaActionPhase,
|
|
7
8
|
FloorIsLavaDifficulty,
|
|
8
|
-
FloorIsLavaPickAction,
|
|
9
9
|
FloorIsLavaRules,
|
|
10
10
|
FloorIsLavaStartAction,
|
|
11
|
+
FloorIsLavaSuccessfulPickAction,
|
|
11
12
|
} from './floor-is-lava-rules';
|
|
12
13
|
|
|
13
14
|
const EDGE_BPS = 200;
|
|
14
15
|
|
|
15
|
-
const startAction = (difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction => ({ phase:
|
|
16
|
+
const startAction = (difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction => ({ phase: FloorIsLavaActionPhase.START, difficulty });
|
|
16
17
|
|
|
17
|
-
const pickAction = ():
|
|
18
|
+
const pickAction = (): FloorIsLavaSuccessfulPickAction => ({
|
|
19
|
+
phase: FloorIsLavaActionPhase.SUCCESSFUL_PICK,
|
|
20
|
+
selectedTile: 0,
|
|
21
|
+
droppedTiles: [],
|
|
22
|
+
multiplier: new BigNumber(0),
|
|
23
|
+
});
|
|
18
24
|
|
|
19
25
|
describe('FloorIsLavaRules.maxRounds', () => {
|
|
20
26
|
it.each([
|
|
@@ -67,6 +73,14 @@ describe('FloorIsLavaRules.getRoundResults', () => {
|
|
|
67
73
|
});
|
|
68
74
|
});
|
|
69
75
|
|
|
76
|
+
describe('FloorIsLavaRules.lastSurvivingTile', () => {
|
|
77
|
+
it('returns the final tile of the full-board drop order — the last to fall', () => {
|
|
78
|
+
const champion = 7;
|
|
79
|
+
const dropOrder = [...Array.from({ length: BOARD_SIZE }, (_, i) => i).filter(tile => tile !== champion), champion];
|
|
80
|
+
expect(FloorIsLavaRules.lastSurvivingTile(new FixedSequenceRng(dropOrder))).toBe(champion);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
70
84
|
describe('FloorIsLavaRules.reconstructRounds', () => {
|
|
71
85
|
it('counts the PICK actions following START as survived rounds', () => {
|
|
72
86
|
const actions = [startAction(FloorIsLavaDifficulty.MEDIUM), pickAction(), pickAction()];
|
|
@@ -88,10 +102,22 @@ describe('FloorIsLavaRules.reconstructRounds', () => {
|
|
|
88
102
|
|
|
89
103
|
it('throws when a CASHOUT action appears in the history', () => {
|
|
90
104
|
expect(() =>
|
|
91
|
-
FloorIsLavaRules.reconstructRounds([
|
|
105
|
+
FloorIsLavaRules.reconstructRounds([
|
|
106
|
+
startAction(FloorIsLavaDifficulty.MEDIUM),
|
|
107
|
+
{ phase: FloorIsLavaActionPhase.CASHOUT, multiplier: new BigNumber(1), lastSurvivingTile: 0 },
|
|
108
|
+
]),
|
|
92
109
|
).toThrow('An active game cannot have a CASHOUT action');
|
|
93
110
|
});
|
|
94
111
|
|
|
112
|
+
it('throws when a BUST_PICK action appears in the history', () => {
|
|
113
|
+
expect(() =>
|
|
114
|
+
FloorIsLavaRules.reconstructRounds([
|
|
115
|
+
startAction(FloorIsLavaDifficulty.MEDIUM),
|
|
116
|
+
{ phase: FloorIsLavaActionPhase.BUST_PICK, selectedTile: 0, droppedTiles: [], lastSurvivingTile: 1 },
|
|
117
|
+
]),
|
|
118
|
+
).toThrow('An active game cannot have a BUST_PICK action');
|
|
119
|
+
});
|
|
120
|
+
|
|
95
121
|
it('throws when survived rounds exceed the difficulty max', () => {
|
|
96
122
|
expect(() => FloorIsLavaRules.reconstructRounds([startAction(FloorIsLavaDifficulty.TOXIC), pickAction(), pickAction()])).toThrow(
|
|
97
123
|
'Reconstructed rounds (2) exceeds max rounds (1)',
|
|
@@ -21,24 +21,43 @@ export const DIFFICULTY_DROP_MAP: Readonly<Record<FloorIsLavaDifficulty, number>
|
|
|
21
21
|
|
|
22
22
|
export const MAX_ROUNDS = DROPPABLE_TILES / Math.min(...Object.values(DIFFICULTY_DROP_MAP));
|
|
23
23
|
|
|
24
|
+
export enum FloorIsLavaActionPhase {
|
|
25
|
+
START = 'START',
|
|
26
|
+
SUCCESSFUL_PICK = 'SUCCESSFUL_PICK',
|
|
27
|
+
BUST_PICK = 'BUST_PICK',
|
|
28
|
+
CASHOUT = 'CASHOUT',
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
export interface FloorIsLavaStartAction {
|
|
25
|
-
phase:
|
|
32
|
+
phase: FloorIsLavaActionPhase.START;
|
|
26
33
|
difficulty: FloorIsLavaDifficulty;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
export interface
|
|
30
|
-
phase:
|
|
36
|
+
export interface FloorIsLavaSuccessfulPickAction {
|
|
37
|
+
phase: FloorIsLavaActionPhase.SUCCESSFUL_PICK;
|
|
31
38
|
selectedTile: number;
|
|
32
39
|
droppedTiles: number[];
|
|
33
40
|
multiplier: BigNumber;
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
export interface FloorIsLavaBustPickAction {
|
|
44
|
+
phase: FloorIsLavaActionPhase.BUST_PICK;
|
|
45
|
+
selectedTile: number;
|
|
46
|
+
droppedTiles: number[];
|
|
47
|
+
lastSurvivingTile: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
36
50
|
export interface FloorIsLavaCashoutAction {
|
|
37
|
-
phase:
|
|
51
|
+
phase: FloorIsLavaActionPhase.CASHOUT;
|
|
38
52
|
multiplier: BigNumber;
|
|
53
|
+
lastSurvivingTile: number;
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
export type FloorIsLavaGameAction =
|
|
56
|
+
export type FloorIsLavaGameAction =
|
|
57
|
+
| FloorIsLavaStartAction
|
|
58
|
+
| FloorIsLavaSuccessfulPickAction
|
|
59
|
+
| FloorIsLavaBustPickAction
|
|
60
|
+
| FloorIsLavaCashoutAction;
|
|
42
61
|
|
|
43
62
|
export class FloorIsLavaRules {
|
|
44
63
|
public static maxRounds(difficulty: FloorIsLavaDifficulty): number {
|
|
@@ -58,6 +77,11 @@ export class FloorIsLavaRules {
|
|
|
58
77
|
return calculateEdgeMultiplier(edge).multipliedBy(BOARD_SIZE).dividedBy(FloorIsLavaRules.tilesRemaining(roundsSurvived, dropsPerRound));
|
|
59
78
|
}
|
|
60
79
|
|
|
80
|
+
public static lastSurvivingTile(generator: RandomNumberGenerator): number {
|
|
81
|
+
const dropOrder = generator.getRandomIntsWithoutReplacement({ min: 0, max: BOARD_SIZE - 1, count: BOARD_SIZE });
|
|
82
|
+
return dropOrder[dropOrder.length - 1];
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
public static getRoundResults(generator: RandomNumberGenerator, dropsPerRound: number, numRounds: number): number[][] {
|
|
62
86
|
const dropSequence = generator.getRandomIntsWithoutReplacement({ min: 0, max: BOARD_SIZE - 1, count: numRounds * dropsPerRound });
|
|
63
87
|
|
|
@@ -70,7 +94,7 @@ export class FloorIsLavaRules {
|
|
|
70
94
|
|
|
71
95
|
public static reconstructRounds(gameActions: FloorIsLavaGameAction[]): { roundsSurvived: number; dropsPerRound: number } {
|
|
72
96
|
const first = gameActions[0];
|
|
73
|
-
if (!first || first.phase !==
|
|
97
|
+
if (!first || first.phase !== FloorIsLavaActionPhase.START) {
|
|
74
98
|
throw new Error('First action must be START');
|
|
75
99
|
}
|
|
76
100
|
|
|
@@ -81,15 +105,17 @@ export class FloorIsLavaRules {
|
|
|
81
105
|
for (let i = 1; i < gameActions.length; i++) {
|
|
82
106
|
const action = gameActions[i];
|
|
83
107
|
switch (action.phase) {
|
|
84
|
-
case
|
|
108
|
+
case FloorIsLavaActionPhase.START:
|
|
85
109
|
throw new Error('START action can only appear as the first action');
|
|
86
|
-
case
|
|
110
|
+
case FloorIsLavaActionPhase.SUCCESSFUL_PICK:
|
|
87
111
|
roundsSurvived++;
|
|
88
112
|
if (roundsSurvived > maxRounds) {
|
|
89
113
|
throw new Error(`Reconstructed rounds (${roundsSurvived}) exceeds max rounds (${maxRounds})`);
|
|
90
114
|
}
|
|
91
115
|
break;
|
|
92
|
-
case
|
|
116
|
+
case FloorIsLavaActionPhase.BUST_PICK:
|
|
117
|
+
throw new Error('An active game cannot have a BUST_PICK action');
|
|
118
|
+
case FloorIsLavaActionPhase.CASHOUT:
|
|
93
119
|
throw new Error('An active game cannot have a CASHOUT action');
|
|
94
120
|
}
|
|
95
121
|
}
|
|
@@ -6,11 +6,12 @@ import {
|
|
|
6
6
|
DIFFICULTY_DROP_MAP,
|
|
7
7
|
DROPPABLE_TILES,
|
|
8
8
|
FloorIsLava,
|
|
9
|
+
FloorIsLavaActionPhase,
|
|
10
|
+
FloorIsLavaAutoBetRoundOutcome,
|
|
9
11
|
FloorIsLavaDifficulty,
|
|
10
|
-
FloorIsLavaPickAction,
|
|
11
|
-
FloorIsLavaRoundOutcome,
|
|
12
12
|
FloorIsLavaRules,
|
|
13
13
|
FloorIsLavaStartAction,
|
|
14
|
+
FloorIsLavaSuccessfulPickAction,
|
|
14
15
|
} from './floor-is-lava';
|
|
15
16
|
import { TrueRandomRng } from './true-random-rng';
|
|
16
17
|
|
|
@@ -18,9 +19,22 @@ const ALL_DIFFICULTIES = [FloorIsLavaDifficulty.EASY, FloorIsLavaDifficulty.MEDI
|
|
|
18
19
|
|
|
19
20
|
const EDGE_BPS = 200;
|
|
20
21
|
|
|
21
|
-
const startAction = (difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction => ({ phase:
|
|
22
|
+
const startAction = (difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction => ({ phase: FloorIsLavaActionPhase.START, difficulty });
|
|
22
23
|
|
|
23
|
-
const pickAction = ():
|
|
24
|
+
const pickAction = (): FloorIsLavaSuccessfulPickAction => ({
|
|
25
|
+
phase: FloorIsLavaActionPhase.SUCCESSFUL_PICK,
|
|
26
|
+
selectedTile: 0,
|
|
27
|
+
droppedTiles: [],
|
|
28
|
+
multiplier: new BigNumber(0),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// A full 49-tile drop order whose leading entries are `prefix` and whose final (last-surviving) tile is `champion`.
|
|
32
|
+
const fullDraw = (prefix: number[], champion: number): number[] => {
|
|
33
|
+
const middle = Array.from({ length: BOARD_SIZE }, (_, i) => i).filter(tile => tile !== champion && !prefix.includes(tile));
|
|
34
|
+
return [...prefix, ...middle, champion];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const fullBoardRng = () => new FixedSequenceRng(Array.from({ length: BOARD_SIZE }, (_, i) => i));
|
|
24
38
|
|
|
25
39
|
const gameWithRounds = (difficulty: FloorIsLavaDifficulty, roundsSurvived: number) =>
|
|
26
40
|
FloorIsLava.fromActions([startAction(difficulty), ...Array.from({ length: roundsSurvived }, () => pickAction())]);
|
|
@@ -75,7 +89,11 @@ describe('FloorIsLava.fromActions', () => {
|
|
|
75
89
|
|
|
76
90
|
it('throws when the history contains a CASHOUT action', () => {
|
|
77
91
|
expect(() =>
|
|
78
|
-
FloorIsLava.fromActions([
|
|
92
|
+
FloorIsLava.fromActions([
|
|
93
|
+
startAction(FloorIsLavaDifficulty.MEDIUM),
|
|
94
|
+
pickAction(),
|
|
95
|
+
{ phase: FloorIsLavaActionPhase.CASHOUT, multiplier: new BigNumber(1), lastSurvivingTile: 0 },
|
|
96
|
+
]),
|
|
79
97
|
).toThrow('An active game cannot have a CASHOUT action');
|
|
80
98
|
});
|
|
81
99
|
|
|
@@ -101,20 +119,20 @@ describe('FloorIsLava.next', () => {
|
|
|
101
119
|
expect(result).toMatchObject({
|
|
102
120
|
isLoss: false,
|
|
103
121
|
isLastTile: false,
|
|
104
|
-
gameAction: { phase: '
|
|
122
|
+
gameAction: { phase: 'SUCCESSFUL_PICK', selectedTile: 5, droppedTiles: [10, 11, 12, 13, 14, 15] },
|
|
105
123
|
});
|
|
106
124
|
expect(result.multiplier.isEqualTo(expectedMultiplier(1, 6))).toBe(true);
|
|
107
125
|
});
|
|
108
126
|
|
|
109
|
-
it('a pick that lands on a dropped tile zeroes the multiplier and
|
|
110
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15]);
|
|
127
|
+
it('a pick that lands on a dropped tile zeroes the multiplier and reveals the surviving tile', () => {
|
|
128
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15], 32));
|
|
111
129
|
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(12, rng, EDGE_BPS));
|
|
112
130
|
|
|
113
131
|
expect(result).toMatchObject({
|
|
114
132
|
isLoss: true,
|
|
115
133
|
isLastTile: false,
|
|
116
134
|
multiplier: new BigNumber(0),
|
|
117
|
-
gameAction: { phase: '
|
|
135
|
+
gameAction: { phase: 'BUST_PICK', selectedTile: 12, droppedTiles: [10, 11, 12, 13, 14, 15], lastSurvivingTile: 32 },
|
|
118
136
|
});
|
|
119
137
|
});
|
|
120
138
|
|
|
@@ -133,10 +151,15 @@ describe('FloorIsLava.next', () => {
|
|
|
133
151
|
});
|
|
134
152
|
|
|
135
153
|
it('a loss deep into a streak still pays zero', () => {
|
|
136
|
-
const rng = new FixedSequenceRng([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 22, 23, 24, 25]);
|
|
154
|
+
const rng = new FixedSequenceRng(fullDraw([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 22, 23, 24, 25], 32));
|
|
137
155
|
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 2).next(22, rng, EDGE_BPS));
|
|
138
156
|
|
|
139
|
-
expect(result).toMatchObject({
|
|
157
|
+
expect(result).toMatchObject({
|
|
158
|
+
isLoss: true,
|
|
159
|
+
isLastTile: false,
|
|
160
|
+
multiplier: new BigNumber(0),
|
|
161
|
+
gameAction: { phase: 'BUST_PICK', lastSurvivingTile: 32 },
|
|
162
|
+
});
|
|
140
163
|
});
|
|
141
164
|
|
|
142
165
|
it('a full clear completes the game at the top multiplier', () => {
|
|
@@ -178,7 +201,7 @@ describe('FloorIsLava.next', () => {
|
|
|
178
201
|
|
|
179
202
|
describe('FloorIsLava — chained real sessions (fromActions replay of actual next() output)', () => {
|
|
180
203
|
it('threads real next() output through fromActions across multiple rounds', () => {
|
|
181
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
204
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 32));
|
|
182
205
|
|
|
183
206
|
const game0 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM)]);
|
|
184
207
|
const round1 = expectOk(game0.next(30, rng, EDGE_BPS));
|
|
@@ -192,12 +215,13 @@ describe('FloorIsLava — chained real sessions (fromActions replay of actual ne
|
|
|
192
215
|
const game2 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), round1.gameAction, round2.gameAction]);
|
|
193
216
|
expect(game2.canCashout()).toBe(true);
|
|
194
217
|
|
|
195
|
-
const cashoutResult = expectOk(game2.cashout(EDGE_BPS));
|
|
218
|
+
const cashoutResult = expectOk(game2.cashout(rng, EDGE_BPS));
|
|
196
219
|
expect(cashoutResult.multiplier).toEqual(round2.multiplier);
|
|
220
|
+
expect(cashoutResult.gameAction.lastSurvivingTile).toBe(32);
|
|
197
221
|
});
|
|
198
222
|
|
|
199
223
|
it('threads a real survived pick into a second round that busts', () => {
|
|
200
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
224
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 32));
|
|
201
225
|
|
|
202
226
|
const game0 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM)]);
|
|
203
227
|
const round1 = expectOk(game0.next(30, rng, EDGE_BPS));
|
|
@@ -205,7 +229,12 @@ describe('FloorIsLava — chained real sessions (fromActions replay of actual ne
|
|
|
205
229
|
const game1 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), round1.gameAction]);
|
|
206
230
|
const round2 = expectOk(game1.next(20, rng, EDGE_BPS));
|
|
207
231
|
|
|
208
|
-
expect(round2).toMatchObject({
|
|
232
|
+
expect(round2).toMatchObject({
|
|
233
|
+
isLoss: true,
|
|
234
|
+
isLastTile: false,
|
|
235
|
+
multiplier: new BigNumber(0),
|
|
236
|
+
gameAction: { phase: 'BUST_PICK', lastSurvivingTile: 32 },
|
|
237
|
+
});
|
|
209
238
|
});
|
|
210
239
|
});
|
|
211
240
|
|
|
@@ -216,7 +245,7 @@ describe('FloorIsLava.cashout', () => {
|
|
|
216
245
|
[FloorIsLavaDifficulty.HARD, 2],
|
|
217
246
|
])('cashing out %s after %i rounds pays the telescoped multiplier', (difficulty, rounds) => {
|
|
218
247
|
const dropsPerRound = difficulty === FloorIsLavaDifficulty.HARD ? 8 : 6;
|
|
219
|
-
const result = expectOk(gameWithRounds(difficulty, rounds).cashout(EDGE_BPS));
|
|
248
|
+
const result = expectOk(gameWithRounds(difficulty, rounds).cashout(fullBoardRng(), EDGE_BPS));
|
|
220
249
|
|
|
221
250
|
expect(result.multiplier.isEqualTo(expectedMultiplier(rounds, dropsPerRound))).toBe(true);
|
|
222
251
|
expect(result.gameAction.phase).toBe('CASHOUT');
|
|
@@ -224,83 +253,87 @@ describe('FloorIsLava.cashout', () => {
|
|
|
224
253
|
});
|
|
225
254
|
|
|
226
255
|
it('returns no-tiles-selected with zero rounds survived', () => {
|
|
227
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).cashout(EDGE_BPS)).toEqual({ ok: false, error: 'no-tiles-selected' });
|
|
256
|
+
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).cashout(fullBoardRng(), EDGE_BPS)).toEqual({ ok: false, error: 'no-tiles-selected' });
|
|
228
257
|
});
|
|
229
258
|
});
|
|
230
259
|
|
|
231
260
|
describe('FloorIsLava.autobet', () => {
|
|
232
261
|
it('surviving every selected round pays the N-round multiplier', () => {
|
|
233
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 40, 41, 42, 43, 44, 45]);
|
|
262
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 40, 41, 42, 43, 44, 45], 0));
|
|
234
263
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31, 32], rng, EDGE_BPS));
|
|
235
264
|
|
|
236
265
|
expect(result).toMatchObject({
|
|
237
266
|
survivedRounds: 3,
|
|
267
|
+
lastSurvivingTile: 0,
|
|
238
268
|
results: [
|
|
239
|
-
{ droppedTiles: [10, 11, 12, 13, 14, 15], outcome:
|
|
240
|
-
{ droppedTiles: [20, 21, 22, 23, 24, 25], outcome:
|
|
241
|
-
{ droppedTiles: [40, 41, 42, 43, 44, 45], outcome:
|
|
269
|
+
{ droppedTiles: [10, 11, 12, 13, 14, 15], outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
270
|
+
{ droppedTiles: [20, 21, 22, 23, 24, 25], outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
271
|
+
{ droppedTiles: [40, 41, 42, 43, 44, 45], outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
242
272
|
],
|
|
243
273
|
});
|
|
244
274
|
expect(result.multiplier.isEqualTo(expectedMultiplier(3, 6))).toBe(true);
|
|
245
275
|
});
|
|
246
276
|
|
|
247
277
|
it('busts and pays zero when a selected tile drops in its own round', () => {
|
|
248
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
278
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
249
279
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 21], rng, EDGE_BPS));
|
|
250
280
|
|
|
251
281
|
expect(result).toMatchObject({
|
|
252
282
|
multiplier: new BigNumber(0),
|
|
253
283
|
survivedRounds: 1,
|
|
284
|
+
lastSurvivingTile: 0,
|
|
254
285
|
results: [
|
|
255
|
-
{ selectedTile: 30, outcome:
|
|
256
|
-
{ selectedTile: 21, droppedTiles: [20, 21, 22, 23, 24, 25], outcome:
|
|
286
|
+
{ selectedTile: 30, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
287
|
+
{ selectedTile: 21, droppedTiles: [20, 21, 22, 23, 24, 25], outcome: FloorIsLavaAutoBetRoundOutcome.LOST },
|
|
257
288
|
],
|
|
258
289
|
});
|
|
259
290
|
});
|
|
260
291
|
|
|
261
292
|
it('forces a cashout (not a bust) when a selected tile had already dropped in an earlier round', () => {
|
|
262
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 40, 41, 42, 43, 44, 45]);
|
|
293
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25, 40, 41, 42, 43, 44, 45], 0));
|
|
263
294
|
// round 3 blindly stands on tile 10, which was already lava since round 1 - the player never actually risked this round's own draw, so they're paid out at what they'd already survived
|
|
264
295
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31, 10], rng, EDGE_BPS));
|
|
265
296
|
|
|
266
297
|
expect(result).toMatchObject({
|
|
267
298
|
survivedRounds: 2,
|
|
299
|
+
lastSurvivingTile: 0,
|
|
268
300
|
results: [
|
|
269
|
-
{ selectedTile: 30, outcome:
|
|
270
|
-
{ selectedTile: 31, outcome:
|
|
271
|
-
{ selectedTile: 10, droppedTiles: [40, 41, 42, 43, 44, 45], outcome:
|
|
301
|
+
{ selectedTile: 30, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
302
|
+
{ selectedTile: 31, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
303
|
+
{ selectedTile: 10, droppedTiles: [40, 41, 42, 43, 44, 45], outcome: FloorIsLavaAutoBetRoundOutcome.CASHED_OUT },
|
|
272
304
|
],
|
|
273
305
|
});
|
|
274
306
|
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6))).toBe(true);
|
|
275
307
|
});
|
|
276
308
|
|
|
277
309
|
it('forces a cashout on the very next round after the pick was already dead', () => {
|
|
278
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
310
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
279
311
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 10], rng, EDGE_BPS));
|
|
280
312
|
|
|
281
313
|
expect(result).toMatchObject({
|
|
282
314
|
survivedRounds: 1,
|
|
315
|
+
lastSurvivingTile: 0,
|
|
283
316
|
results: [
|
|
284
|
-
{ selectedTile: 30, outcome:
|
|
285
|
-
{ selectedTile: 10, droppedTiles: [20, 21, 22, 23, 24, 25], outcome:
|
|
317
|
+
{ selectedTile: 30, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
318
|
+
{ selectedTile: 10, droppedTiles: [20, 21, 22, 23, 24, 25], outcome: FloorIsLavaAutoBetRoundOutcome.CASHED_OUT },
|
|
286
319
|
],
|
|
287
320
|
});
|
|
288
321
|
expect(result.multiplier.isEqualTo(expectedMultiplier(1, 6))).toBe(true);
|
|
289
322
|
});
|
|
290
323
|
|
|
291
324
|
it('allows revisiting a still-standing tile across rounds', () => {
|
|
292
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
325
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
293
326
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [5, 5], rng, EDGE_BPS));
|
|
294
327
|
|
|
295
|
-
expect(result).toMatchObject({ survivedRounds: 2 });
|
|
328
|
+
expect(result).toMatchObject({ survivedRounds: 2, lastSurvivingTile: 0 });
|
|
296
329
|
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6))).toBe(true);
|
|
297
330
|
});
|
|
298
331
|
|
|
299
332
|
it('a full-length sequence full-clears at the top multiplier', () => {
|
|
300
|
-
const rng = new FixedSequenceRng(Array.from({ length:
|
|
333
|
+
const rng = new FixedSequenceRng(Array.from({ length: BOARD_SIZE }, (_, i) => i));
|
|
301
334
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.TOXIC, [DROPPABLE_TILES], rng, EDGE_BPS));
|
|
302
335
|
|
|
303
|
-
expect(result).toMatchObject({ survivedRounds: 1, multiplier: new BigNumber('48.02') });
|
|
336
|
+
expect(result).toMatchObject({ survivedRounds: 1, multiplier: new BigNumber('48.02'), lastSurvivingTile: DROPPABLE_TILES });
|
|
304
337
|
});
|
|
305
338
|
|
|
306
339
|
it('returns invalid-selected-tiles-count when the sequence exceeds the difficulty max rounds', () => {
|
|
@@ -327,7 +360,7 @@ describe('FloorIsLava.autobet', () => {
|
|
|
327
360
|
|
|
328
361
|
describe('FloorIsLava — edge parameter', () => {
|
|
329
362
|
it.each([100, 500, 1000])('cashout applies the given edge (%i bps), not a hardcoded value', edgeBps => {
|
|
330
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(edgeBps));
|
|
363
|
+
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(fullBoardRng(), edgeBps));
|
|
331
364
|
expect(result.multiplier.isEqualTo(expectedMultiplier(3, 6, edgeBps))).toBe(true);
|
|
332
365
|
});
|
|
333
366
|
|
|
@@ -338,13 +371,13 @@ describe('FloorIsLava — edge parameter', () => {
|
|
|
338
371
|
});
|
|
339
372
|
|
|
340
373
|
it.each([100, 500, 1000])('autobet applies the given edge (%i bps) on a full survive', edgeBps => {
|
|
341
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
374
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
342
375
|
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31], rng, edgeBps));
|
|
343
376
|
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6, edgeBps))).toBe(true);
|
|
344
377
|
});
|
|
345
378
|
|
|
346
379
|
it('rejects an edge below the platform minimum (100 bps) on cashout', () => {
|
|
347
|
-
expect(() => gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
380
|
+
expect(() => gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(fullBoardRng(), 99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
348
381
|
});
|
|
349
382
|
|
|
350
383
|
it('rejects an edge below the platform minimum (100 bps) on a surviving next()', () => {
|
|
@@ -353,7 +386,7 @@ describe('FloorIsLava — edge parameter', () => {
|
|
|
353
386
|
});
|
|
354
387
|
|
|
355
388
|
it('rejects an edge below the platform minimum (100 bps) on a surviving autobet', () => {
|
|
356
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
389
|
+
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
357
390
|
expect(() => FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31], rng, 99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
358
391
|
});
|
|
359
392
|
});
|
|
@@ -429,7 +462,7 @@ describe('FloorIsLava — exhaustive multiplier & RTP across every endpoint', ()
|
|
|
429
462
|
expect(multiplier.toNumber()).toBeCloseTo(houseEdge.dividedBy(chance).toNumber(), 9);
|
|
430
463
|
|
|
431
464
|
// the game surface (fromActions + cashout) returns the same endpoint multiplier
|
|
432
|
-
expect(expectOk(gameWithRounds(difficulty, k).cashout(EDGE_BPS)).multiplier.isEqualTo(multiplier)).toBe(true);
|
|
465
|
+
expect(expectOk(gameWithRounds(difficulty, k).cashout(fullBoardRng(), EDGE_BPS)).multiplier.isEqualTo(multiplier)).toBe(true);
|
|
433
466
|
|
|
434
467
|
// RTP == winChance * payout is the house edge at EVERY endpoint, not just on average
|
|
435
468
|
expect(chance.multipliedBy(multiplier).toNumber()).toBeCloseTo(houseEdge.toNumber(), 9);
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import BigNumber from 'bignumber.js';
|
|
2
2
|
import {
|
|
3
3
|
DIFFICULTY_DROP_MAP,
|
|
4
|
+
FloorIsLavaActionPhase,
|
|
5
|
+
FloorIsLavaBustPickAction,
|
|
4
6
|
FloorIsLavaCashoutAction,
|
|
5
7
|
FloorIsLavaDifficulty,
|
|
6
8
|
FloorIsLavaGameAction,
|
|
7
|
-
FloorIsLavaPickAction,
|
|
8
9
|
FloorIsLavaRules,
|
|
9
10
|
FloorIsLavaStartAction,
|
|
11
|
+
FloorIsLavaSuccessfulPickAction,
|
|
10
12
|
} from './floor-is-lava-rules';
|
|
11
13
|
import { RandomNumberGenerator } from './random-number-generator.interface';
|
|
12
14
|
|
|
@@ -15,7 +17,13 @@ export * from './floor-is-lava-rules';
|
|
|
15
17
|
export type FloorIsLavaNextError = 'tile-already-dropped' | 'no-tiles-remaining' | 'invalid-tile';
|
|
16
18
|
|
|
17
19
|
export type FloorIsLavaNextResult =
|
|
18
|
-
| {
|
|
20
|
+
| {
|
|
21
|
+
ok: true;
|
|
22
|
+
multiplier: BigNumber;
|
|
23
|
+
gameAction: FloorIsLavaSuccessfulPickAction | FloorIsLavaBustPickAction;
|
|
24
|
+
isLoss: boolean;
|
|
25
|
+
isLastTile: boolean;
|
|
26
|
+
}
|
|
19
27
|
| { ok: false; error: FloorIsLavaNextError };
|
|
20
28
|
|
|
21
29
|
export type FloorIsLavaCashoutError = 'no-tiles-selected';
|
|
@@ -24,22 +32,22 @@ export type FloorIsLavaCashoutResult =
|
|
|
24
32
|
| { ok: true; multiplier: BigNumber; gameAction: FloorIsLavaCashoutAction }
|
|
25
33
|
| { ok: false; error: FloorIsLavaCashoutError };
|
|
26
34
|
|
|
27
|
-
export enum
|
|
35
|
+
export enum FloorIsLavaAutoBetRoundOutcome {
|
|
28
36
|
SURVIVED = 'SURVIVED',
|
|
29
37
|
LOST = 'LOST',
|
|
30
38
|
CASHED_OUT = 'CASHED_OUT',
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
export interface
|
|
41
|
+
export interface FloorIsLavaAutoBetRoundResult {
|
|
34
42
|
selectedTile: number;
|
|
35
43
|
droppedTiles: number[];
|
|
36
|
-
outcome:
|
|
44
|
+
outcome: FloorIsLavaAutoBetRoundOutcome;
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
export type FloorIsLavaAutoBetError = 'invalid-selected-tiles-count' | 'invalid-tile';
|
|
40
48
|
|
|
41
49
|
export type FloorIsLavaAutoBetResult =
|
|
42
|
-
| { ok: true; multiplier: BigNumber; results:
|
|
50
|
+
| { ok: true; multiplier: BigNumber; results: FloorIsLavaAutoBetRoundResult[]; survivedRounds: number; lastSurvivingTile: number }
|
|
43
51
|
| { ok: false; error: FloorIsLavaAutoBetError };
|
|
44
52
|
|
|
45
53
|
export class FloorIsLava {
|
|
@@ -49,7 +57,7 @@ export class FloorIsLava {
|
|
|
49
57
|
) {}
|
|
50
58
|
|
|
51
59
|
public static createAction(difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction {
|
|
52
|
-
return { phase:
|
|
60
|
+
return { phase: FloorIsLavaActionPhase.START, difficulty };
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
public static fromActions(gameActions: FloorIsLavaGameAction[]): FloorIsLava {
|
|
@@ -83,21 +91,25 @@ export class FloorIsLava {
|
|
|
83
91
|
const isLastTile = FloorIsLavaRules.tilesRemaining(roundsSurvivedAfter, this.dropsPerRound) <= 1;
|
|
84
92
|
const multiplier = isLoss ? BigNumber(0) : FloorIsLavaRules.multiplier(roundsSurvivedAfter, this.dropsPerRound, edge);
|
|
85
93
|
|
|
94
|
+
const gameAction: FloorIsLavaSuccessfulPickAction | FloorIsLavaBustPickAction = isLoss
|
|
95
|
+
? {
|
|
96
|
+
phase: FloorIsLavaActionPhase.BUST_PICK,
|
|
97
|
+
selectedTile,
|
|
98
|
+
droppedTiles: thisRoundDrops,
|
|
99
|
+
lastSurvivingTile: FloorIsLavaRules.lastSurvivingTile(generator),
|
|
100
|
+
}
|
|
101
|
+
: { phase: FloorIsLavaActionPhase.SUCCESSFUL_PICK, selectedTile, droppedTiles: thisRoundDrops, multiplier };
|
|
102
|
+
|
|
86
103
|
return {
|
|
87
104
|
ok: true,
|
|
88
105
|
multiplier,
|
|
89
|
-
gameAction
|
|
90
|
-
phase: 'PICK',
|
|
91
|
-
selectedTile,
|
|
92
|
-
droppedTiles: thisRoundDrops,
|
|
93
|
-
multiplier,
|
|
94
|
-
},
|
|
106
|
+
gameAction,
|
|
95
107
|
isLoss,
|
|
96
108
|
isLastTile,
|
|
97
109
|
};
|
|
98
110
|
}
|
|
99
111
|
|
|
100
|
-
public cashout(edge: number): FloorIsLavaCashoutResult {
|
|
112
|
+
public cashout(generator: RandomNumberGenerator, edge: number): FloorIsLavaCashoutResult {
|
|
101
113
|
if (!this.canCashout()) {
|
|
102
114
|
return { ok: false, error: 'no-tiles-selected' };
|
|
103
115
|
}
|
|
@@ -107,7 +119,7 @@ export class FloorIsLava {
|
|
|
107
119
|
return {
|
|
108
120
|
ok: true,
|
|
109
121
|
multiplier,
|
|
110
|
-
gameAction: { phase:
|
|
122
|
+
gameAction: { phase: FloorIsLavaActionPhase.CASHOUT, multiplier, lastSurvivingTile: FloorIsLavaRules.lastSurvivingTile(generator) },
|
|
111
123
|
};
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -130,8 +142,9 @@ export class FloorIsLava {
|
|
|
130
142
|
|
|
131
143
|
const rounds = FloorIsLavaRules.getRoundResults(generator, dropsPerRound, selectedTiles.length);
|
|
132
144
|
|
|
133
|
-
const results:
|
|
145
|
+
const results: FloorIsLavaAutoBetRoundResult[] = [];
|
|
134
146
|
let survivedRounds = 0;
|
|
147
|
+
const lastSurvivingTile = FloorIsLavaRules.lastSurvivingTile(generator);
|
|
135
148
|
|
|
136
149
|
for (let round = 0; round < selectedTiles.length; round++) {
|
|
137
150
|
const selectedTile = selectedTiles[round];
|
|
@@ -141,20 +154,21 @@ export class FloorIsLava {
|
|
|
141
154
|
const priorDrops = results.flatMap(result => result.droppedTiles);
|
|
142
155
|
const isUnselectableDropTile = priorDrops.includes(selectedTile);
|
|
143
156
|
if (isUnselectableDropTile) {
|
|
144
|
-
results.push({ selectedTile, droppedTiles, outcome:
|
|
157
|
+
results.push({ selectedTile, droppedTiles, outcome: FloorIsLavaAutoBetRoundOutcome.CASHED_OUT });
|
|
145
158
|
return {
|
|
146
159
|
ok: true,
|
|
147
160
|
multiplier: FloorIsLavaRules.multiplier(survivedRounds, dropsPerRound, edge),
|
|
148
161
|
results,
|
|
149
162
|
survivedRounds,
|
|
163
|
+
lastSurvivingTile,
|
|
150
164
|
};
|
|
151
165
|
}
|
|
152
166
|
|
|
153
167
|
const isLoss = droppedTiles.includes(selectedTile);
|
|
154
|
-
results.push({ selectedTile, droppedTiles, outcome: isLoss ?
|
|
168
|
+
results.push({ selectedTile, droppedTiles, outcome: isLoss ? FloorIsLavaAutoBetRoundOutcome.LOST : FloorIsLavaAutoBetRoundOutcome.SURVIVED });
|
|
155
169
|
|
|
156
170
|
if (isLoss) {
|
|
157
|
-
return { ok: true, multiplier: BigNumber(0), results, survivedRounds };
|
|
171
|
+
return { ok: true, multiplier: BigNumber(0), results, survivedRounds, lastSurvivingTile };
|
|
158
172
|
}
|
|
159
173
|
survivedRounds++;
|
|
160
174
|
}
|
|
@@ -164,6 +178,7 @@ export class FloorIsLava {
|
|
|
164
178
|
multiplier: FloorIsLavaRules.multiplier(survivedRounds, dropsPerRound, edge),
|
|
165
179
|
results,
|
|
166
180
|
survivedRounds,
|
|
181
|
+
lastSurvivingTile,
|
|
167
182
|
};
|
|
168
183
|
}
|
|
169
184
|
}
|
package/src/lottery/lottery.ts
CHANGED
|
@@ -17,7 +17,7 @@ export const LOTTERY_VERSION_RECORD: Record<LotteryVersion, LotteryConfig> = {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
export class Lottery {
|
|
20
|
-
static readonly version = LotteryVersion.
|
|
20
|
+
static readonly version = LotteryVersion.V1;
|
|
21
21
|
|
|
22
22
|
static getConfig(version: LotteryVersion): LotteryConfig {
|
|
23
23
|
return LOTTERY_VERSION_RECORD[version];
|
|
@@ -638,21 +638,12 @@ export class MarketLayout {
|
|
|
638
638
|
marketDisplayType: SportsMarketDisplayType;
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
-
export enum MarketInfoVariantKey {
|
|
642
|
-
MAP = 'MAP',
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
export type MapVariant = string;
|
|
646
|
-
|
|
647
641
|
export class BaseDisplayGroup {
|
|
648
642
|
groupKey: string;
|
|
649
643
|
groupName: string;
|
|
650
644
|
groupSortName: string;
|
|
651
645
|
selectionGroups: SelectionGroup[] | null;
|
|
652
646
|
layout: MarketLayout;
|
|
653
|
-
variantData: Nullable<{
|
|
654
|
-
[MarketInfoVariantKey.MAP]?: MapVariant; // used for map market of oddin
|
|
655
|
-
}>;
|
|
656
647
|
is2UpAvailable: boolean;
|
|
657
648
|
}
|
|
658
649
|
|
|
@@ -720,10 +711,16 @@ export class SportsNewMarketOddsUpdate extends SportsMarketOddsBase {
|
|
|
720
711
|
expiryTime: Nullable<Date>;
|
|
721
712
|
}
|
|
722
713
|
|
|
714
|
+
export class SgmSubGroup {
|
|
715
|
+
group: SportsMarketGroup;
|
|
716
|
+
weight: number;
|
|
717
|
+
}
|
|
718
|
+
|
|
723
719
|
export class SportsMarketInfo {
|
|
724
720
|
odds: SportsNewMarketOddsUpdate[];
|
|
725
721
|
display: DisplayGroup[];
|
|
726
722
|
updatedAt: Nullable<Date>;
|
|
723
|
+
sgmSubGroups: Nullable<SgmSubGroup[]>;
|
|
727
724
|
}
|
|
728
725
|
|
|
729
726
|
export class SportsMarketSubInfo extends SportsMarketInfo {
|