Slot Engine is in Beta - Stable, but may contain minor bugs
Slot EngineSlot Engine

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:

  1. It loads __build__/publish_files/lookUpTable_<mode>_0.csv of the mode.
  2. It draws a row by weight with the random function of the configuration. If the session forces an outcome, it picks the row by the rules of Forced outcomes instead.
  3. It reads the drawn book from the published books of the mode with BookReader from @slot-engine/core. The reader keeps the last four decompressed chunks in memory.
  4. It returns the events of the book as round.state and 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:

ValueComputation
round.payoutMultiplierpayoutCents / 100
round.payoutround(amount × payoutCents / 100)
round.activepayout > 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:

  1. It converts the multiplier to cents with Math.round(payoutMultiplier * 100).
  2. 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.
  3. It picks one of the books with that payout. Every book has the same chance, and the pick uses the random function.

For a lookup table with the payouts 0x, 1.5x and 3x:

Forced multiplierPlayed payoutWhy
1.51.5xExact match.
2.251.5x1.5x and 3x are equally close. The lower payout wins.
2.43x3x is closer than 1.5x.
1003x3x 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.

scripted.ts
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,
})
OptionDescription
lookupTablePath(mode)Path of the published lookup table of a mode.
booksA BookSource: read(mode, bookId) resolves with { events } or undefined.
randomReturns 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.

On this page