Game Math
How the LGS draws outcomes from your simulated math.
Outcomes come from the simulation
The LGS does not run your game logic. It replays the books that the simulation wrote, the same way the RGS does after you upload the math.
When you register a SlotGame, through the games option or through the panel, the LGS wraps it in a PublishedMath. On every play:
- It loads
__build__/publish_files/lookUpTable_<mode>_0.csvof the mode. - It draws a row by weight with the
randomfunction of the configuration. If the session forces an outcome, it picks the row by the rules of Forced outcomes instead. - It reads the drawn book from the published books of the mode with
BookReaderfrom@slot-engine/core. The reader keeps the last four decompressed chunks in memory. - It returns the events of the book as
round.stateand computes the payout from the row.
Because the draw uses the published weights, the RTP of play equals the RTP of the lookup table. Optimize the game before you judge its feel in a front end.
After you simulate or optimize the game again, the next play uses the new math without a restart.
Payouts
The lookup table stores payouts in cents of the base bet, 250 is 2.5x. The LGS converts them:
| Value | Computation |
|---|---|
round.payoutMultiplier | payoutCents / 100 |
round.payout | round(amount × payoutCents / 100) |
round.active | payout > 0 |
Forced outcomes
A session can force the outcome of its plays through PUT /lgs/sessions/:id/force or the Play Game tab of the panel.
PublishedMath then skips the weighted draw and picks the book by one of two rules.
Both rules look at every row of the lookup table, including rows with a weight of 0.
While a force is set, play ignores the weights, so the RTP of play no longer matches the lookup table.
Force a book ID
{ "bookId": 123 } plays book 123 of the played mode, with its events and its payout from the lookup table.
Book IDs belong to one mode, so book 123 of base and book 123 of bonus are different books.
If the lookup table of the played mode has no row for the book, pick throws unknown-book, and EngineLgs answers the bet with status 400 and ERR_VAL.
Force a payout multiplier
{ "payoutMultiplier": 2.5 } plays a book with the payout closest to 2.5x. PublishedMath finds the book in three steps:
- It converts the multiplier to cents with
Math.round(payoutMultiplier * 100). - It finds the payout closest to that value among all payouts in the lookup table of the mode. If a lower and a higher payout are equally close, the lower payout wins.
- It picks one of the books with that payout. Every book has the same chance, and the pick uses the
randomfunction.
For a lookup table with the payouts 0x, 1.5x and 3x:
| Forced multiplier | Played payout | Why |
|---|---|---|
1.5 | 1.5x | Exact match. |
2.25 | 1.5x | 1.5x and 3x are equally close. The lower payout wins. |
2.4 | 3x | 3x is closer than 1.5x. |
100 | 3x | 3x is the highest payout and the closest. |
Custom math
The LGS asks a GameMath for outcomes. Register a game with your own GameMath to play without build files, for example in tests or to script a sequence of outcomes.
interface GameMath {
pick(mode: string, force?: ForcedOutcome): Promise<Outcome>
read(mode: string, bookId: number): Promise<Outcome | undefined>
}
type ForcedOutcome = { bookId: number } | { payoutMultiplier: number }
interface Outcome {
bookId: number
/** Payout in cents of the base bet: 100 is 1x. */
payoutCents: number
events: unknown[]
}The LGS passes the force of the session as force, and undefined when the session forces nothing.
A GameMath may ignore it, like the scripted example below.
read returns the outcome of one book for bet replay, and undefined when the lookup table of the mode has no row for the book.
import { EngineLgs, InMemoryStorage, type LgsGame, type Outcome } from "@slot-engine/lgs"
const payouts = [0, 0, 250, 0, 1000]
let next = 0
function outcome(bookId: number): Outcome {
const payoutCents = payouts[bookId] ?? 0
return { bookId, payoutCents, events: [{ type: "reveal", payout: payoutCents }] }
}
const scripted: LgsGame = {
id: "scripted",
name: "Scripted",
modes: {
base: { name: "base", cost: 1, rtp: 0.96, isBonusBuy: false, isFeature: false },
},
math: {
async pick() {
return outcome(next++ % payouts.length)
},
async read(_mode, bookId) {
return bookId < payouts.length ? outcome(bookId) : undefined
},
},
}
const lgs = new EngineLgs({ storage: new InMemoryStorage() })
lgs.registerGame(scripted)LgsMode carries the fields of a game mode that the LGS needs: name, cost, rtp, isBonusBuy and isFeature.
PublishedMath
fromSlotGame(game, random) builds the LgsGame of a SlotGame with a PublishedMath.
Use PublishedMath directly to read the math from another location or through another book source:
import { BookReader } from "@slot-engine/core"
import { PublishedMath } from "@slot-engine/lgs"
const math = new PublishedMath({
lookupTablePath: (mode) => `./math/lookUpTable_${mode}_0.csv`,
books: new BookReader(game, { maxCachedChunks: 4 }),
random: Math.random,
})| Option | Description |
|---|---|
lookupTablePath(mode) | Path of the published lookup table of a mode. |
books | A BookSource: read(mode, bookId) resolves with { events } or undefined. |
random | Returns a number in [0, 1). |
A missing lookup table throws missing-math.
A missing book throws missing-book, which EngineLgs reports as ERR_GEN.
A forced book that the lookup table does not list throws unknown-book, reported as ERR_VAL.
read returns undefined for such a book, and EngineLgs answers the replay with 404.
Use of AI on this page: Some texts were improved with or written by AI.