shufflecom-calculations 4.2.0 → 4.2.2
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/coinflip/coinflip-classic-progressive.d.ts +1 -1
- package/lib/games/coinflip/coinflip-classic.d.ts +1 -1
- package/lib/games/coinflip/coinflip-rules.d.ts +1 -1
- package/lib/games/coinflip/coinflip-target.d.ts +1 -1
- package/lib/games/floor-is-lava/floor-is-lava-board.d.ts +8 -0
- package/lib/games/floor-is-lava/floor-is-lava-board.js +37 -0
- package/lib/games/floor-is-lava/floor-is-lava-board.js.map +1 -0
- package/lib/games/floor-is-lava/floor-is-lava-rules.d.ts +69 -0
- package/lib/games/floor-is-lava/floor-is-lava-rules.js +133 -0
- package/lib/games/floor-is-lava/floor-is-lava-rules.js.map +1 -0
- package/lib/games/floor-is-lava/floor-is-lava.d.ts +34 -0
- package/lib/games/floor-is-lava/floor-is-lava.js +106 -0
- package/lib/games/floor-is-lava/floor-is-lava.js.map +1 -0
- package/lib/games/rng/fixed-rng.js.map +1 -0
- package/lib/games/rng/random-number-generator.interface.js.map +1 -0
- package/lib/games/rng/shuffle-random-number-generator.d.ts +21 -0
- package/lib/games/rng/shuffle-random-number-generator.js +76 -0
- package/lib/games/rng/shuffle-random-number-generator.js.map +1 -0
- package/lib/games/rng/true-random-rng.js.map +1 -0
- package/lib/index.d.ts +7 -2
- package/lib/index.js +7 -2
- package/lib/index.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/games/coinflip/coinflip-classic-progressive.spec.ts +14 -3
- package/src/games/coinflip/coinflip-classic-progressive.ts +1 -1
- package/src/games/coinflip/coinflip-classic.spec.ts +1 -1
- package/src/games/coinflip/coinflip-classic.ts +1 -1
- package/src/games/coinflip/coinflip-rules.spec.ts +1 -1
- package/src/games/coinflip/coinflip-rules.ts +1 -1
- package/src/games/coinflip/coinflip-target.spec.ts +1 -1
- package/src/games/coinflip/coinflip-target.ts +1 -1
- package/src/games/floor-is-lava/floor-is-lava-board.spec.ts +61 -0
- package/src/games/floor-is-lava/floor-is-lava-board.ts +41 -0
- package/src/games/floor-is-lava/floor-is-lava-rules.spec.ts +316 -0
- package/src/games/floor-is-lava/floor-is-lava-rules.ts +217 -0
- package/src/games/floor-is-lava/floor-is-lava.spec.ts +455 -0
- package/src/games/floor-is-lava/floor-is-lava.ts +138 -0
- package/src/games/rng/shuffle-random-number-generator.ts +100 -0
- package/src/index.ts +7 -2
- package/lib/games/fixed-rng.js.map +0 -1
- package/lib/games/floor-is-lava-rules.d.ts +0 -52
- package/lib/games/floor-is-lava-rules.js +0 -82
- package/lib/games/floor-is-lava-rules.js.map +0 -1
- package/lib/games/floor-is-lava.d.ts +0 -56
- package/lib/games/floor-is-lava.js +0 -134
- package/lib/games/floor-is-lava.js.map +0 -1
- package/lib/games/random-number-generator.interface.js.map +0 -1
- package/lib/games/true-random-rng.js.map +0 -1
- package/src/games/floor-is-lava-rules.spec.ts +0 -126
- package/src/games/floor-is-lava-rules.ts +0 -125
- package/src/games/floor-is-lava.spec.ts +0 -502
- package/src/games/floor-is-lava.ts +0 -184
- /package/lib/games/{fixed-rng.d.ts → rng/fixed-rng.d.ts} +0 -0
- /package/lib/games/{fixed-rng.js → rng/fixed-rng.js} +0 -0
- /package/lib/games/{random-number-generator.interface.d.ts → rng/random-number-generator.interface.d.ts} +0 -0
- /package/lib/games/{random-number-generator.interface.js → rng/random-number-generator.interface.js} +0 -0
- /package/lib/games/{true-random-rng.d.ts → rng/true-random-rng.d.ts} +0 -0
- /package/lib/games/{true-random-rng.js → rng/true-random-rng.js} +0 -0
- /package/src/games/{fixed-rng.ts → rng/fixed-rng.ts} +0 -0
- /package/src/games/{random-number-generator.interface.ts → rng/random-number-generator.interface.ts} +0 -0
- /package/src/games/{true-random-rng.ts → rng/true-random-rng.ts} +0 -0
|
@@ -1,502 +0,0 @@
|
|
|
1
|
-
import BigNumber from 'bignumber.js';
|
|
2
|
-
import { calculateEdgeMultiplier } from '../utils/edge';
|
|
3
|
-
import { FixedSequenceRng } from './fixed-rng';
|
|
4
|
-
import {
|
|
5
|
-
BOARD_SIZE,
|
|
6
|
-
DIFFICULTY_DROP_MAP,
|
|
7
|
-
DROPPABLE_TILES,
|
|
8
|
-
FloorIsLava,
|
|
9
|
-
FloorIsLavaActionPhase,
|
|
10
|
-
FloorIsLavaAutoBetRoundOutcome,
|
|
11
|
-
FloorIsLavaDifficulty,
|
|
12
|
-
FloorIsLavaRules,
|
|
13
|
-
FloorIsLavaStartAction,
|
|
14
|
-
FloorIsLavaSuccessfulPickAction,
|
|
15
|
-
} from './floor-is-lava';
|
|
16
|
-
import { TrueRandomRng } from './true-random-rng';
|
|
17
|
-
|
|
18
|
-
const ALL_DIFFICULTIES = [FloorIsLavaDifficulty.EASY, FloorIsLavaDifficulty.MEDIUM, FloorIsLavaDifficulty.HARD, FloorIsLavaDifficulty.TOXIC];
|
|
19
|
-
|
|
20
|
-
const EDGE_BPS = 200;
|
|
21
|
-
|
|
22
|
-
const startAction = (difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction => ({ phase: FloorIsLavaActionPhase.START, difficulty });
|
|
23
|
-
|
|
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));
|
|
38
|
-
|
|
39
|
-
const gameWithRounds = (difficulty: FloorIsLavaDifficulty, roundsSurvived: number) =>
|
|
40
|
-
FloorIsLava.fromActions([startAction(difficulty), ...Array.from({ length: roundsSurvived }, () => pickAction())]);
|
|
41
|
-
|
|
42
|
-
const expectOk = <T extends { ok: boolean }>(result: T): Extract<T, { ok: true }> => {
|
|
43
|
-
if (!result.ok) {
|
|
44
|
-
throw new Error(`Expected an ok result but got: ${JSON.stringify(result)}`);
|
|
45
|
-
}
|
|
46
|
-
return result as Extract<T, { ok: true }>;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const expectedMultiplier = (roundsSurvived: number, dropsPerRound: number, edgeBps: number = EDGE_BPS) =>
|
|
50
|
-
calculateEdgeMultiplier(edgeBps)
|
|
51
|
-
.multipliedBy(BOARD_SIZE)
|
|
52
|
-
.dividedBy(BOARD_SIZE - roundsSurvived * dropsPerRound);
|
|
53
|
-
|
|
54
|
-
describe('FloorIsLava.createAction', () => {
|
|
55
|
-
it('returns a START action carrying the difficulty', () => {
|
|
56
|
-
expect(FloorIsLava.createAction(FloorIsLavaDifficulty.MEDIUM)).toEqual({ phase: 'START', difficulty: FloorIsLavaDifficulty.MEDIUM });
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('FloorIsLava.fromActions', () => {
|
|
61
|
-
describe('valid histories', () => {
|
|
62
|
-
it('a start-only history cannot be cashed out', () => {
|
|
63
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).canCashout()).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('accumulates surviving rounds into cashout eligibility', () => {
|
|
67
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).canCashout()).toBe(true);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('accepts a history at exactly the max round count', () => {
|
|
71
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.EASY, FloorIsLavaRules.maxRounds(FloorIsLavaDifficulty.EASY)).canCashout()).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
describe('validation errors', () => {
|
|
76
|
-
it('throws on an empty history', () => {
|
|
77
|
-
expect(() => FloorIsLava.fromActions([])).toThrow('First action must be START');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('throws when the first action is not START', () => {
|
|
81
|
-
expect(() => FloorIsLava.fromActions([pickAction()])).toThrow('First action must be START');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('throws when a START action appears after the first position', () => {
|
|
85
|
-
expect(() =>
|
|
86
|
-
FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), pickAction(), startAction(FloorIsLavaDifficulty.MEDIUM)]),
|
|
87
|
-
).toThrow('START action can only appear as the first action');
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('throws when the history contains a CASHOUT action', () => {
|
|
91
|
-
expect(() =>
|
|
92
|
-
FloorIsLava.fromActions([
|
|
93
|
-
startAction(FloorIsLavaDifficulty.MEDIUM),
|
|
94
|
-
pickAction(),
|
|
95
|
-
{ phase: FloorIsLavaActionPhase.CASHOUT, multiplier: new BigNumber(1), lastSurvivingTile: 0 },
|
|
96
|
-
]),
|
|
97
|
-
).toThrow('An active game cannot have a CASHOUT action');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('throws when survived rounds exceed the difficulty max', () => {
|
|
101
|
-
// TOXIC drops all 48 in one round, so max rounds is 1
|
|
102
|
-
expect(() => FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.TOXIC), pickAction(), pickAction()])).toThrow(
|
|
103
|
-
'Reconstructed rounds (2) exceeds max rounds (1)',
|
|
104
|
-
);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('FloorIsLava.next', () => {
|
|
110
|
-
it.each([-1, 49, 1.5, NaN])('rejects an out-of-range selectedTile (%s)', tile => {
|
|
111
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13]);
|
|
112
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(tile, rng, EDGE_BPS)).toEqual({ ok: false, error: 'invalid-tile' });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('a surviving first pick keeps the game open and pays the 1-round multiplier', () => {
|
|
116
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15]);
|
|
117
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(5, rng, EDGE_BPS));
|
|
118
|
-
|
|
119
|
-
expect(result).toMatchObject({
|
|
120
|
-
isLoss: false,
|
|
121
|
-
isLastTile: false,
|
|
122
|
-
gameAction: { phase: 'SUCCESSFUL_PICK', selectedTile: 5, droppedTiles: [10, 11, 12, 13, 14, 15] },
|
|
123
|
-
});
|
|
124
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(1, 6))).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
|
|
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));
|
|
129
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(12, rng, EDGE_BPS));
|
|
130
|
-
|
|
131
|
-
expect(result).toMatchObject({
|
|
132
|
-
isLoss: true,
|
|
133
|
-
isLastTile: false,
|
|
134
|
-
multiplier: new BigNumber(0),
|
|
135
|
-
gameAction: { phase: 'BUST_PICK', selectedTile: 12, droppedTiles: [10, 11, 12, 13, 14, 15], lastSurvivingTile: 32 },
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('drops this round from the growing suffix and excludes prior-round drops', () => {
|
|
140
|
-
// rounds 1 & 2 already survived (12 tiles gone), this is round 3
|
|
141
|
-
const rng = new FixedSequenceRng([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 22, 23, 24, 25]);
|
|
142
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 2).next(30, rng, EDGE_BPS));
|
|
143
|
-
|
|
144
|
-
expect(result).toMatchObject({ isLoss: false, gameAction: { droppedTiles: [20, 21, 22, 23, 24, 25] } });
|
|
145
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(3, 6))).toBe(true);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('a pick on a tile dropped in an earlier round is rejected', () => {
|
|
149
|
-
const rng = new FixedSequenceRng([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 20, 21, 22, 23, 24, 25]);
|
|
150
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 2).next(2, rng, EDGE_BPS)).toEqual({ ok: false, error: 'tile-already-dropped' });
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('a loss deep into a streak still pays zero', () => {
|
|
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));
|
|
155
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 2).next(22, rng, EDGE_BPS));
|
|
156
|
-
|
|
157
|
-
expect(result).toMatchObject({
|
|
158
|
-
isLoss: true,
|
|
159
|
-
isLastTile: false,
|
|
160
|
-
multiplier: new BigNumber(0),
|
|
161
|
-
gameAction: { phase: 'BUST_PICK', lastSurvivingTile: 32 },
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('a full clear completes the game at the top multiplier', () => {
|
|
166
|
-
const rng = new FixedSequenceRng(Array.from({ length: DROPPABLE_TILES }, (_, i) => i));
|
|
167
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.TOXIC, 0).next(DROPPABLE_TILES, rng, EDGE_BPS));
|
|
168
|
-
|
|
169
|
-
expect(result).toMatchObject({ isLoss: false, isLastTile: true, multiplier: new BigNumber('48.02') });
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('completes the game when a win reduces the board to a single tile', () => {
|
|
173
|
-
// EASY drops 4 per round across 12 rounds; surviving to the last round leaves exactly one standing tile
|
|
174
|
-
const rng = new FixedSequenceRng(Array.from({ length: DROPPABLE_TILES }, (_, i) => i));
|
|
175
|
-
const maxRounds = FloorIsLavaRules.maxRounds(FloorIsLavaDifficulty.EASY);
|
|
176
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.EASY, maxRounds - 1).next(DROPPABLE_TILES, rng, EDGE_BPS));
|
|
177
|
-
|
|
178
|
-
expect(result).toMatchObject({ isLoss: false, isLastTile: true, multiplier: new BigNumber('48.02') });
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('returns no-tiles-remaining when there are no tiles left to pick', () => {
|
|
182
|
-
const rng = new FixedSequenceRng([]);
|
|
183
|
-
const maxRounds = FloorIsLavaRules.maxRounds(FloorIsLavaDifficulty.EASY);
|
|
184
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.EASY, maxRounds).next(0, rng, EDGE_BPS)).toEqual({
|
|
185
|
-
ok: false,
|
|
186
|
-
error: 'no-tiles-remaining',
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('reveals fresh tiles each round via the growing cumulative draw', () => {
|
|
191
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25]);
|
|
192
|
-
|
|
193
|
-
const round1 = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(30, rng, EDGE_BPS));
|
|
194
|
-
const round2 = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 1).next(31, rng, EDGE_BPS));
|
|
195
|
-
|
|
196
|
-
expect(round1.gameAction.droppedTiles).toEqual([10, 11, 12, 13, 14, 15]);
|
|
197
|
-
expect(round2.gameAction.droppedTiles).toEqual([20, 21, 22, 23, 24, 25]);
|
|
198
|
-
expect(round1.gameAction.droppedTiles).not.toEqual(round2.gameAction.droppedTiles);
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
describe('FloorIsLava — chained real sessions (fromActions replay of actual next() output)', () => {
|
|
203
|
-
it('threads real next() output through fromActions across multiple rounds', () => {
|
|
204
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 32));
|
|
205
|
-
|
|
206
|
-
const game0 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM)]);
|
|
207
|
-
const round1 = expectOk(game0.next(30, rng, EDGE_BPS));
|
|
208
|
-
expect(round1.isLoss).toBe(false);
|
|
209
|
-
|
|
210
|
-
const game1 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), round1.gameAction]);
|
|
211
|
-
const round2 = expectOk(game1.next(31, rng, EDGE_BPS));
|
|
212
|
-
expect(round2).toMatchObject({ isLoss: false, gameAction: { droppedTiles: [20, 21, 22, 23, 24, 25] } });
|
|
213
|
-
expect(round2.gameAction.droppedTiles).not.toEqual(round1.gameAction.droppedTiles);
|
|
214
|
-
|
|
215
|
-
const game2 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), round1.gameAction, round2.gameAction]);
|
|
216
|
-
expect(game2.canCashout()).toBe(true);
|
|
217
|
-
|
|
218
|
-
const cashoutResult = expectOk(game2.cashout(rng, EDGE_BPS));
|
|
219
|
-
expect(cashoutResult.multiplier).toEqual(round2.multiplier);
|
|
220
|
-
expect(cashoutResult.gameAction.lastSurvivingTile).toBe(32);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('threads a real survived pick into a second round that busts', () => {
|
|
224
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 32));
|
|
225
|
-
|
|
226
|
-
const game0 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM)]);
|
|
227
|
-
const round1 = expectOk(game0.next(30, rng, EDGE_BPS));
|
|
228
|
-
|
|
229
|
-
const game1 = FloorIsLava.fromActions([startAction(FloorIsLavaDifficulty.MEDIUM), round1.gameAction]);
|
|
230
|
-
const round2 = expectOk(game1.next(20, rng, EDGE_BPS));
|
|
231
|
-
|
|
232
|
-
expect(round2).toMatchObject({
|
|
233
|
-
isLoss: true,
|
|
234
|
-
isLastTile: false,
|
|
235
|
-
multiplier: new BigNumber(0),
|
|
236
|
-
gameAction: { phase: 'BUST_PICK', lastSurvivingTile: 32 },
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('FloorIsLava.cashout', () => {
|
|
242
|
-
it.each([
|
|
243
|
-
[FloorIsLavaDifficulty.MEDIUM, 1],
|
|
244
|
-
[FloorIsLavaDifficulty.MEDIUM, 3],
|
|
245
|
-
[FloorIsLavaDifficulty.HARD, 2],
|
|
246
|
-
])('cashing out %s after %i rounds pays the telescoped multiplier', (difficulty, rounds) => {
|
|
247
|
-
const dropsPerRound = difficulty === FloorIsLavaDifficulty.HARD ? 8 : 6;
|
|
248
|
-
const result = expectOk(gameWithRounds(difficulty, rounds).cashout(fullBoardRng(), EDGE_BPS));
|
|
249
|
-
|
|
250
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(rounds, dropsPerRound))).toBe(true);
|
|
251
|
-
expect(result.gameAction.phase).toBe('CASHOUT');
|
|
252
|
-
expect(result.gameAction.multiplier.isEqualTo(expectedMultiplier(rounds, dropsPerRound))).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('returns no-tiles-selected with zero rounds survived', () => {
|
|
256
|
-
expect(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).cashout(fullBoardRng(), EDGE_BPS)).toEqual({ ok: false, error: 'no-tiles-selected' });
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('FloorIsLava.autobet', () => {
|
|
261
|
-
it('surviving every selected round pays the N-round multiplier', () => {
|
|
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));
|
|
263
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31, 32], rng, EDGE_BPS));
|
|
264
|
-
|
|
265
|
-
expect(result).toMatchObject({
|
|
266
|
-
survivedRounds: 3,
|
|
267
|
-
lastSurvivingTile: 0,
|
|
268
|
-
results: [
|
|
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 },
|
|
272
|
-
],
|
|
273
|
-
});
|
|
274
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(3, 6))).toBe(true);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('busts and pays zero when a selected tile drops in its own round', () => {
|
|
278
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
279
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 21], rng, EDGE_BPS));
|
|
280
|
-
|
|
281
|
-
expect(result).toMatchObject({
|
|
282
|
-
multiplier: new BigNumber(0),
|
|
283
|
-
survivedRounds: 1,
|
|
284
|
-
lastSurvivingTile: 0,
|
|
285
|
-
results: [
|
|
286
|
-
{ selectedTile: 30, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
287
|
-
{ selectedTile: 21, droppedTiles: [20, 21, 22, 23, 24, 25], outcome: FloorIsLavaAutoBetRoundOutcome.LOST },
|
|
288
|
-
],
|
|
289
|
-
});
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('forces a cashout (not a bust) when a selected tile had already dropped in an earlier round', () => {
|
|
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));
|
|
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
|
|
295
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31, 10], rng, EDGE_BPS));
|
|
296
|
-
|
|
297
|
-
expect(result).toMatchObject({
|
|
298
|
-
survivedRounds: 2,
|
|
299
|
-
lastSurvivingTile: 0,
|
|
300
|
-
results: [
|
|
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 },
|
|
304
|
-
],
|
|
305
|
-
});
|
|
306
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6))).toBe(true);
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('forces a cashout on the very next round after the pick was already dead', () => {
|
|
310
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
311
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 10], rng, EDGE_BPS));
|
|
312
|
-
|
|
313
|
-
expect(result).toMatchObject({
|
|
314
|
-
survivedRounds: 1,
|
|
315
|
-
lastSurvivingTile: 0,
|
|
316
|
-
results: [
|
|
317
|
-
{ selectedTile: 30, outcome: FloorIsLavaAutoBetRoundOutcome.SURVIVED },
|
|
318
|
-
{ selectedTile: 10, droppedTiles: [20, 21, 22, 23, 24, 25], outcome: FloorIsLavaAutoBetRoundOutcome.CASHED_OUT },
|
|
319
|
-
],
|
|
320
|
-
});
|
|
321
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(1, 6))).toBe(true);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('allows revisiting a still-standing tile across rounds', () => {
|
|
325
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
326
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [5, 5], rng, EDGE_BPS));
|
|
327
|
-
|
|
328
|
-
expect(result).toMatchObject({ survivedRounds: 2, lastSurvivingTile: 0 });
|
|
329
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6))).toBe(true);
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('a full-length sequence full-clears at the top multiplier', () => {
|
|
333
|
-
const rng = new FixedSequenceRng(Array.from({ length: BOARD_SIZE }, (_, i) => i));
|
|
334
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.TOXIC, [DROPPABLE_TILES], rng, EDGE_BPS));
|
|
335
|
-
|
|
336
|
-
expect(result).toMatchObject({ survivedRounds: 1, multiplier: new BigNumber('48.02'), lastSurvivingTile: DROPPABLE_TILES });
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it('returns invalid-selected-tiles-count when the sequence exceeds the difficulty max rounds', () => {
|
|
340
|
-
const rng = new FixedSequenceRng([0, 1]);
|
|
341
|
-
expect(FloorIsLava.autobet(FloorIsLavaDifficulty.TOXIC, [10, 20], rng, EDGE_BPS)).toEqual({
|
|
342
|
-
ok: false,
|
|
343
|
-
error: 'invalid-selected-tiles-count',
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('returns invalid-selected-tiles-count on an empty sequence', () => {
|
|
348
|
-
const rng = new FixedSequenceRng([]);
|
|
349
|
-
expect(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [], rng, EDGE_BPS)).toEqual({
|
|
350
|
-
ok: false,
|
|
351
|
-
error: 'invalid-selected-tiles-count',
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it.each([-1, 49, 1.5, NaN])('returns invalid-tile when any selected tile (%s) is out of range', tile => {
|
|
356
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13]);
|
|
357
|
-
expect(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [5, tile], rng, EDGE_BPS)).toEqual({ ok: false, error: 'invalid-tile' });
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe('FloorIsLava — edge parameter', () => {
|
|
362
|
-
it.each([100, 500, 1000])('cashout applies the given edge (%i bps), not a hardcoded value', edgeBps => {
|
|
363
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(fullBoardRng(), edgeBps));
|
|
364
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(3, 6, edgeBps))).toBe(true);
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
it.each([100, 500, 1000])('next applies the given edge (%i bps) to a surviving pick', edgeBps => {
|
|
368
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15]);
|
|
369
|
-
const result = expectOk(gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(5, rng, edgeBps));
|
|
370
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(1, 6, edgeBps))).toBe(true);
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
it.each([100, 500, 1000])('autobet applies the given edge (%i bps) on a full survive', edgeBps => {
|
|
374
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
375
|
-
const result = expectOk(FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31], rng, edgeBps));
|
|
376
|
-
expect(result.multiplier.isEqualTo(expectedMultiplier(2, 6, edgeBps))).toBe(true);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it('rejects an edge below the platform minimum (100 bps) on cashout', () => {
|
|
380
|
-
expect(() => gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 3).cashout(fullBoardRng(), 99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('rejects an edge below the platform minimum (100 bps) on a surviving next()', () => {
|
|
384
|
-
const rng = new FixedSequenceRng([10, 11, 12, 13, 14, 15]);
|
|
385
|
-
expect(() => gameWithRounds(FloorIsLavaDifficulty.MEDIUM, 0).next(5, rng, 99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
it('rejects an edge below the platform minimum (100 bps) on a surviving autobet', () => {
|
|
389
|
-
const rng = new FixedSequenceRng(fullDraw([10, 11, 12, 13, 14, 15, 20, 21, 22, 23, 24, 25], 0));
|
|
390
|
-
expect(() => FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, [30, 31], rng, 99)).toThrow('Exceeded Minimum edge allowed on platform');
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
describe('FloorIsLavaRules.maxRounds', () => {
|
|
395
|
-
it.each([
|
|
396
|
-
[FloorIsLavaDifficulty.EASY, 12],
|
|
397
|
-
[FloorIsLavaDifficulty.MEDIUM, 8],
|
|
398
|
-
[FloorIsLavaDifficulty.HARD, 6],
|
|
399
|
-
[FloorIsLavaDifficulty.TOXIC, 1],
|
|
400
|
-
])('%s allows %i rounds', (difficulty, expected) => {
|
|
401
|
-
expect(FloorIsLavaRules.maxRounds(difficulty)).toBe(expected);
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
describe('FloorIsLavaRules.isValidTile', () => {
|
|
406
|
-
it.each([0, 1, BOARD_SIZE - 1])('accepts an in-range integer tile (%s)', tile => {
|
|
407
|
-
expect(FloorIsLavaRules.isValidTile(tile)).toBe(true);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it.each([-1, BOARD_SIZE, 1.5, NaN, Infinity])('rejects an out-of-range or non-integer tile (%s)', tile => {
|
|
411
|
-
expect(FloorIsLavaRules.isValidTile(tile)).toBe(false);
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
describe('DIFFICULTY_DROP_MAP', () => {
|
|
416
|
-
it.each(Object.entries(DIFFICULTY_DROP_MAP))('%s drops per round is a factor of DROPPABLE_TILES', (_difficulty, dropsPerRound) => {
|
|
417
|
-
expect(DROPPABLE_TILES % dropsPerRound).toBe(0);
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
describe('FloorIsLavaRules.getRoundResults', () => {
|
|
422
|
-
it('chunks the drawn sequence into per-round groups of dropsPerRound', () => {
|
|
423
|
-
const rng = new FixedSequenceRng([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
|
424
|
-
expect(FloorIsLavaRules.getRoundResults(rng, 4, 3)).toEqual([
|
|
425
|
-
[0, 1, 2, 3],
|
|
426
|
-
[4, 5, 6, 7],
|
|
427
|
-
[8, 9, 10, 11],
|
|
428
|
-
]);
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it('is prefix-stable — asking for more rounds keeps earlier rounds identical', () => {
|
|
432
|
-
const rng = new FixedSequenceRng([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]);
|
|
433
|
-
const twoRounds = FloorIsLavaRules.getRoundResults(rng, 4, 2);
|
|
434
|
-
const threeRounds = FloorIsLavaRules.getRoundResults(rng, 4, 3);
|
|
435
|
-
expect(threeRounds.slice(0, 2)).toEqual(twoRounds);
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
describe('FloorIsLava — exhaustive multiplier & RTP across every endpoint', () => {
|
|
440
|
-
const houseEdge = calculateEdgeMultiplier(EDGE_BPS); // 0.98 == 98% RTP
|
|
441
|
-
|
|
442
|
-
// Win chance derived independently, round by round, as the product of each round's survival
|
|
443
|
-
// probability (standingBefore - X) / standingBefore. Makes no assumption about telescoping.
|
|
444
|
-
const winChance = (rounds: number, dropsPerRound: number): BigNumber => {
|
|
445
|
-
let chance = new BigNumber(1);
|
|
446
|
-
for (let r = 1; r <= rounds; r++) {
|
|
447
|
-
const standingBefore = BOARD_SIZE - (r - 1) * dropsPerRound;
|
|
448
|
-
chance = chance.multipliedBy(standingBefore - dropsPerRound).dividedBy(standingBefore);
|
|
449
|
-
}
|
|
450
|
-
return chance;
|
|
451
|
-
};
|
|
452
|
-
|
|
453
|
-
it.each(ALL_DIFFICULTIES)('%s: every reachable endpoint pays fair odds and returns 98% RTP', difficulty => {
|
|
454
|
-
const dropsPerRound = DIFFICULTY_DROP_MAP[difficulty];
|
|
455
|
-
const maxRounds = FloorIsLavaRules.maxRounds(difficulty);
|
|
456
|
-
|
|
457
|
-
for (let k = 1; k <= maxRounds; k++) {
|
|
458
|
-
const chance = winChance(k, dropsPerRound);
|
|
459
|
-
const multiplier = FloorIsLavaRules.multiplier(k, dropsPerRound, EDGE_BPS);
|
|
460
|
-
|
|
461
|
-
// fair payout inverts the win chance and applies the edge once: houseEdge / winChance
|
|
462
|
-
expect(multiplier.toNumber()).toBeCloseTo(houseEdge.dividedBy(chance).toNumber(), 9);
|
|
463
|
-
|
|
464
|
-
// the game surface (fromActions + cashout) returns the same endpoint multiplier
|
|
465
|
-
expect(expectOk(gameWithRounds(difficulty, k).cashout(fullBoardRng(), EDGE_BPS)).multiplier.isEqualTo(multiplier)).toBe(true);
|
|
466
|
-
|
|
467
|
-
// RTP == winChance * payout is the house edge at EVERY endpoint, not just on average
|
|
468
|
-
expect(chance.multipliedBy(multiplier).toNumber()).toBeCloseTo(houseEdge.toNumber(), 9);
|
|
469
|
-
}
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
it('every difficulty full-clears to the same 48.02x ceiling', () => {
|
|
473
|
-
for (const difficulty of ALL_DIFFICULTIES) {
|
|
474
|
-
const dropsPerRound = DIFFICULTY_DROP_MAP[difficulty];
|
|
475
|
-
const maxRounds = FloorIsLavaRules.maxRounds(difficulty);
|
|
476
|
-
expect(FloorIsLavaRules.multiplier(maxRounds, dropsPerRound, EDGE_BPS)).toEqual(new BigNumber('48.02'));
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
describe('FloorIsLava.autobet — empirical RTP regression guard', () => {
|
|
482
|
-
const TRIALS = 80_000;
|
|
483
|
-
const RTP_TOLERANCE = 0.05;
|
|
484
|
-
|
|
485
|
-
it.each([2, 3, 5])('MEDIUM autobet with %i distinct tile picks averages ~98%% RTP over real randomness', rounds => {
|
|
486
|
-
const selectedTiles = Array.from({ length: rounds }, (_, i) => i);
|
|
487
|
-
|
|
488
|
-
let totalPayout = new BigNumber(0);
|
|
489
|
-
for (let trial = 0; trial < TRIALS; trial++) {
|
|
490
|
-
const result = FloorIsLava.autobet(FloorIsLavaDifficulty.MEDIUM, selectedTiles, new TrueRandomRng(), EDGE_BPS);
|
|
491
|
-
if (result.ok) {
|
|
492
|
-
totalPayout = totalPayout.plus(result.multiplier);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const empiricalRTP = totalPayout.dividedBy(TRIALS).toNumber();
|
|
497
|
-
const intendedRTP = calculateEdgeMultiplier(EDGE_BPS).toNumber();
|
|
498
|
-
|
|
499
|
-
expect(empiricalRTP).toBeGreaterThan(intendedRTP - RTP_TOLERANCE);
|
|
500
|
-
expect(empiricalRTP).toBeLessThan(intendedRTP + RTP_TOLERANCE);
|
|
501
|
-
});
|
|
502
|
-
});
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
import BigNumber from 'bignumber.js';
|
|
2
|
-
import {
|
|
3
|
-
DIFFICULTY_DROP_MAP,
|
|
4
|
-
FloorIsLavaActionPhase,
|
|
5
|
-
FloorIsLavaBustPickAction,
|
|
6
|
-
FloorIsLavaCashoutAction,
|
|
7
|
-
FloorIsLavaDifficulty,
|
|
8
|
-
FloorIsLavaGameAction,
|
|
9
|
-
FloorIsLavaRules,
|
|
10
|
-
FloorIsLavaStartAction,
|
|
11
|
-
FloorIsLavaSuccessfulPickAction,
|
|
12
|
-
} from './floor-is-lava-rules';
|
|
13
|
-
import { RandomNumberGenerator } from './random-number-generator.interface';
|
|
14
|
-
|
|
15
|
-
export * from './floor-is-lava-rules';
|
|
16
|
-
|
|
17
|
-
export type FloorIsLavaNextError = 'tile-already-dropped' | 'no-tiles-remaining' | 'invalid-tile';
|
|
18
|
-
|
|
19
|
-
export type FloorIsLavaNextResult =
|
|
20
|
-
| {
|
|
21
|
-
ok: true;
|
|
22
|
-
multiplier: BigNumber;
|
|
23
|
-
gameAction: FloorIsLavaSuccessfulPickAction | FloorIsLavaBustPickAction;
|
|
24
|
-
isLoss: boolean;
|
|
25
|
-
isLastTile: boolean;
|
|
26
|
-
}
|
|
27
|
-
| { ok: false; error: FloorIsLavaNextError };
|
|
28
|
-
|
|
29
|
-
export type FloorIsLavaCashoutError = 'no-tiles-selected';
|
|
30
|
-
|
|
31
|
-
export type FloorIsLavaCashoutResult =
|
|
32
|
-
| { ok: true; multiplier: BigNumber; gameAction: FloorIsLavaCashoutAction }
|
|
33
|
-
| { ok: false; error: FloorIsLavaCashoutError };
|
|
34
|
-
|
|
35
|
-
export enum FloorIsLavaAutoBetRoundOutcome {
|
|
36
|
-
SURVIVED = 'SURVIVED',
|
|
37
|
-
LOST = 'LOST',
|
|
38
|
-
CASHED_OUT = 'CASHED_OUT',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface FloorIsLavaAutoBetRoundResult {
|
|
42
|
-
selectedTile: number;
|
|
43
|
-
droppedTiles: number[];
|
|
44
|
-
outcome: FloorIsLavaAutoBetRoundOutcome;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type FloorIsLavaAutoBetError = 'invalid-selected-tiles-count' | 'invalid-tile';
|
|
48
|
-
|
|
49
|
-
export type FloorIsLavaAutoBetResult =
|
|
50
|
-
| { ok: true; multiplier: BigNumber; results: FloorIsLavaAutoBetRoundResult[]; survivedRounds: number; lastSurvivingTile: number }
|
|
51
|
-
| { ok: false; error: FloorIsLavaAutoBetError };
|
|
52
|
-
|
|
53
|
-
export class FloorIsLava {
|
|
54
|
-
private constructor(
|
|
55
|
-
private readonly roundsSurvived: number,
|
|
56
|
-
private readonly dropsPerRound: number,
|
|
57
|
-
) {}
|
|
58
|
-
|
|
59
|
-
public static createAction(difficulty: FloorIsLavaDifficulty): FloorIsLavaStartAction {
|
|
60
|
-
return { phase: FloorIsLavaActionPhase.START, difficulty };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
public static fromActions(gameActions: FloorIsLavaGameAction[]): FloorIsLava {
|
|
64
|
-
const { roundsSurvived, dropsPerRound } = FloorIsLavaRules.reconstructRounds(gameActions);
|
|
65
|
-
return new FloorIsLava(roundsSurvived, dropsPerRound);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public canCashout(): boolean {
|
|
69
|
-
return this.roundsSurvived > 0;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
public next(selectedTile: number, generator: RandomNumberGenerator, edge: number): FloorIsLavaNextResult {
|
|
73
|
-
if (!FloorIsLavaRules.isValidTile(selectedTile)) {
|
|
74
|
-
return { ok: false, error: 'invalid-tile' };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (FloorIsLavaRules.tilesRemaining(this.roundsSurvived, this.dropsPerRound) <= 1) {
|
|
78
|
-
return { ok: false, error: 'no-tiles-remaining' };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const rounds = FloorIsLavaRules.getRoundResults(generator, this.dropsPerRound, this.roundsSurvived + 1);
|
|
82
|
-
const priorDrops = rounds.slice(0, this.roundsSurvived).flat();
|
|
83
|
-
if (priorDrops.includes(selectedTile)) {
|
|
84
|
-
return { ok: false, error: 'tile-already-dropped' };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const thisRoundDrops = rounds[this.roundsSurvived];
|
|
88
|
-
const isLoss = thisRoundDrops.includes(selectedTile);
|
|
89
|
-
|
|
90
|
-
const roundsSurvivedAfter = isLoss ? this.roundsSurvived : this.roundsSurvived + 1;
|
|
91
|
-
const isLastTile = FloorIsLavaRules.tilesRemaining(roundsSurvivedAfter, this.dropsPerRound) <= 1;
|
|
92
|
-
const multiplier = isLoss ? BigNumber(0) : FloorIsLavaRules.multiplier(roundsSurvivedAfter, this.dropsPerRound, edge);
|
|
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
|
-
|
|
103
|
-
return {
|
|
104
|
-
ok: true,
|
|
105
|
-
multiplier,
|
|
106
|
-
gameAction,
|
|
107
|
-
isLoss,
|
|
108
|
-
isLastTile,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
public cashout(generator: RandomNumberGenerator, edge: number): FloorIsLavaCashoutResult {
|
|
113
|
-
if (!this.canCashout()) {
|
|
114
|
-
return { ok: false, error: 'no-tiles-selected' };
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const multiplier = FloorIsLavaRules.multiplier(this.roundsSurvived, this.dropsPerRound, edge);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
ok: true,
|
|
121
|
-
multiplier,
|
|
122
|
-
gameAction: { phase: FloorIsLavaActionPhase.CASHOUT, multiplier, lastSurvivingTile: FloorIsLavaRules.lastSurvivingTile(generator) },
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
public static autobet(
|
|
127
|
-
difficulty: FloorIsLavaDifficulty,
|
|
128
|
-
selectedTiles: number[],
|
|
129
|
-
generator: RandomNumberGenerator,
|
|
130
|
-
edge: number,
|
|
131
|
-
): FloorIsLavaAutoBetResult {
|
|
132
|
-
if (selectedTiles.some(tile => !FloorIsLavaRules.isValidTile(tile))) {
|
|
133
|
-
return { ok: false, error: 'invalid-tile' };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const dropsPerRound = DIFFICULTY_DROP_MAP[difficulty];
|
|
137
|
-
const maxRounds = FloorIsLavaRules.maxRounds(difficulty);
|
|
138
|
-
|
|
139
|
-
if (selectedTiles.length < 1 || selectedTiles.length > maxRounds) {
|
|
140
|
-
return { ok: false, error: 'invalid-selected-tiles-count' };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const rounds = FloorIsLavaRules.getRoundResults(generator, dropsPerRound, selectedTiles.length);
|
|
144
|
-
|
|
145
|
-
const results: FloorIsLavaAutoBetRoundResult[] = [];
|
|
146
|
-
let survivedRounds = 0;
|
|
147
|
-
const lastSurvivingTile = FloorIsLavaRules.lastSurvivingTile(generator);
|
|
148
|
-
|
|
149
|
-
for (let round = 0; round < selectedTiles.length; round++) {
|
|
150
|
-
const selectedTile = selectedTiles[round];
|
|
151
|
-
|
|
152
|
-
const droppedTiles = rounds[round];
|
|
153
|
-
|
|
154
|
-
const priorDrops = results.flatMap(result => result.droppedTiles);
|
|
155
|
-
const isUnselectableDropTile = priorDrops.includes(selectedTile);
|
|
156
|
-
if (isUnselectableDropTile) {
|
|
157
|
-
results.push({ selectedTile, droppedTiles, outcome: FloorIsLavaAutoBetRoundOutcome.CASHED_OUT });
|
|
158
|
-
return {
|
|
159
|
-
ok: true,
|
|
160
|
-
multiplier: FloorIsLavaRules.multiplier(survivedRounds, dropsPerRound, edge),
|
|
161
|
-
results,
|
|
162
|
-
survivedRounds,
|
|
163
|
-
lastSurvivingTile,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const isLoss = droppedTiles.includes(selectedTile);
|
|
168
|
-
results.push({ selectedTile, droppedTiles, outcome: isLoss ? FloorIsLavaAutoBetRoundOutcome.LOST : FloorIsLavaAutoBetRoundOutcome.SURVIVED });
|
|
169
|
-
|
|
170
|
-
if (isLoss) {
|
|
171
|
-
return { ok: true, multiplier: BigNumber(0), results, survivedRounds, lastSurvivingTile };
|
|
172
|
-
}
|
|
173
|
-
survivedRounds++;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
ok: true,
|
|
178
|
-
multiplier: FloorIsLavaRules.multiplier(survivedRounds, dropsPerRound, edge),
|
|
179
|
-
results,
|
|
180
|
-
survivedRounds,
|
|
181
|
-
lastSurvivingTile,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|