DocsGames
Limbo
How one float becomes a multiplier, where the house edge goes in, and why about one round in fifty lands on exactly 1.00.
Limbo is the first mapper with a parameter that changes the answer. Same seeds, same nonce, three different declared edges:
import { play } from '@galabet/fair';
const seeds = {
serverSeed: '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d',
clientSeed: 'galabet',
nonce: 42,
};
for (const houseEdge of [0, 0.01, 0.05]) {
const { result } = await play({ game: 'limbo', params: { houseEdge }, ...seeds });
console.log(houseEdge, result);
}
0 1.78
0.01 1.76
0.05 1.69
So a Limbo result means nothing without the edge it was calculated under, and that's why houseEdge travels in the record's params. Leave it out and the library assumes 0.01.
Formula
raw = 1e8 / (float × 1e8 + 1)
result = floor(raw × (1 − houseEdge) × 100) / 100, and never below 1.00
A small float gives a big multiplier. With our float of 0.5611712262034416, raw is about 1.78, the 1% edge takes it to 1.764, and the floor leaves 1.76.
The + 1 stops a float of zero from dividing by zero. It also sets the ceiling.
import { limbo } from '@galabet/fair/games';
console.log(limbo(0)); // the smallest float
console.log(limbo(0, 0)); // same, with no edge
console.log(limbo(0.5));
console.log(limbo(0.99));
console.log(limbo(1 - 2 ** -32)); // the largest float
99000000
100000000
1.97
1
1
Ninety-nine million, once in 4,294,967,296 rounds. Don't build a jackpot animation for it.
House Edge and Results of 1.00
Look at the bottom of that list. Both of the last two floats give 1.00, and they're not rare: any float above roughly 0.98 produces a raw value under 1.0203, the edge pulls it under 1.01, the floor takes it to 1.00, and a player who needed anything above 1.00 has lost. Count them:
import { play } from '@galabet/fair';
const serverSeed = '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
let ones = 0, atLeastTwo = 0;
for (let nonce = 0; nonce < 20000; nonce++) {
const { result } = await play({ game: 'limbo', params: { houseEdge: 0.01 }, serverSeed, clientSeed: 'galabet', nonce });
if (result === 1) ones++;
if (result >= 2) atLeastTwo++;
}
console.log(`exactly 1.00: ${ones} of 20000`);
console.log(`2.00 or more: ${atLeastTwo} of 20000`);
exactly 1.00: 412 of 20000
2.00 or more: 9930 of 20000
The second line is the useful one for anybody checking a site's claims. The chance of reaching a target is close to (1 − houseEdge) / target, so with a 1% edge a 2.00 target should come in a little under half the time, 49.5%. The sample above landed on 49.65%. That's the whole business model of the game, stated in one division.
Settling Bets
Settlement isn't part of the library. The demo's rule is that the bet wins when result >= target, equality included, and it pays floor(stake × target) in whole credits. Notice that this is the opposite convention from Dice, where equality loses. Whichever you choose, write it where players can read it.
Known Pitfalls
houseEdge Is Not Validated by play
Pass houseEdge: 2 and you get 1.00 forever, with no error, because a negative product is floored up to the minimum. inspectRecord is stricter and accepts 0 to 0.5. Validate the parameter yourself before it gets anywhere near a bet.
Floating-Point Order in Ports
Use doubles, multiply the float by 1e8 and add 1 before dividing, apply the edge, multiply by 100, floor, divide by 100. Rearranging those steps changes the last digit on some rounds. Python with plain floats matches the reference:
import hashlib
import hmac
import math
seed = "5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d"
for nonce in (42, 43, 44):
digest = hmac.new(seed.encode(), f"galabet:{nonce}:0".encode(), hashlib.sha256).digest()
f = int.from_bytes(digest[:4], "big") / 2**32
raw = 1e8 / (f * 1e8 + 1)
print(nonce, max(1.0, math.floor(raw * (1 - 0.01) * 100) / 100))
42 1.76
43 2.13
44 1.38
import { play } from '@galabet/fair';
const serverSeed = '5c1f7d3e8a2b4c6d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d';
for (const nonce of [42, 43, 44]) {
const { result } = await play({ game: 'limbo', params: { houseEdge: 0.01 }, serverSeed, clientSeed: 'galabet', nonce });
console.log(nonce, result);
}
42 1.76
43 2.13
44 1.38
Three rounds agreeing isn't proof. The vectors file has 336 Limbo rounds at edges of 0, 0.01 and 0.04, and that is.
Errors
| Thrown | When |
|---|---|
Nothing, for a bad houseEdge | See above. play doesn't check it |
houseEdge: expected a number from 0 to 0.5. | inspectRecord, edge out of range |
params.rows: not supported for limbo. | inspectRecord, a parameter that belongs to another game |
