# Quick Start (/docs/lgs) Todo # Core Concepts (/docs/core/core-concepts) *** ## How Slot Engine Works Slot Engine's Core library is designed specifically for Stake Engine integration, following similar principles to Stake's Math SDK. When the [RGS](#rgs-remote-gaming-server) determines a game round outcome, it randomly selects an entry from a **weighted list of pre-calculated results**. All possible game outcomes are **predetermined during the build process** and stored in JSON and CSV files. Slot Engine generates these files, allowing you to upload them directly to Stake Engine. Games are configured using [game modes](#game-mode). For example, you might configure a "base" game mode (normal bet) and a "bonus" game mode (a.k.a. bonus buy). A single game flow implementation handles all simulations, regardless of the active game mode. [Result sets](#result-set) are configured for each game mode to determine whether a [simulation](#simulation) result is accepted or retried until specific acceptance criteria are met. Multiple result sets ensure diverse and exceptional outcomes across simulations. Simulation results may produce a suboptimal [RTP](#rtp). This is acceptable, as an [optimization](#optimization) process typically follows to adjust the values accordingly. ## Terminology ### RGS (Remote Gaming Server) A remote gaming server that communicates with the client application, accepting bets and returning results to players. ### Game Mode A game mode defines a **purchasable** game behavior, similar to "bet modes" in the Stake Math SDK. Game modes can vary in cost, outcomes, symbols, and mechanics. Common game mode patterns include: * **Base game** - The standard gameplay at 1x bet multiplier * **Bonus game** - e.g. 100x bet for instant free spins * **Super bonus** - e.g. 500x bet for instant super free spins Players can bet on the base game or purchase bonus features directly. A "base" mode can still include free spins triggered naturally, while a dedicated "bonus" mode allows immediate access for an extra cost. [Further reading](/docs/core/config/game-modes) ### Simulation A simulation executes the game flow implementation—equivalent to **pressing the spin button on a slot**. Each simulation result is stored temporarily until it's either accepted or rejected. When accepted, the result is stored permanently, the state resets, and a new simulation begins. When rejected, the simulation retries until acceptance criteria are met. [Further reading](/docs/core/game-tasks/simulation) ### Result Set Defining result sets (similar to "distributions" in the Math SDK) is crucial for **simulating very rare scenarios**. Even if you configure a simulation to run 1.000.000 spins of a game mode, a max win could be rare enough that it never occurs naturally. Result sets allow you to **enforce specific outcome quotas**, ensuring simulations retry until the defined criteria are met. [Further reading](/docs/core/config/result-sets) ### Optimization The process of recalculating weights for all outcomes to achieve a specific target [RTP](#rtp). [Further reading](/docs/core/game-tasks/optimization) ### RTP Return to Player (RTP) is a percentage that represents the theoretical amount of money a player can expect to win back over an extended period of gameplay. # Quick Start (/docs/core) *** ## Introduction Slot Engine is a family of TypeScript libraries for building, simulating and testing slot games. } title="Slot Engine Core"> Library for configuring and simulating slot games. Produces output compatible with Stake Engine / Stake RGS. } title="Slot Engine LGS"> Local gaming server. Test your game locally without uploading to Stake Engine and save time during development. ### Further Reading For a more in-depth introduction to Slot Engine and its features, check out ["What is Slot Engine?"](/docs/core/what-is-slot-engine). Unsure which is right for you? ["Slot Engine vs Stake Math SDK"](/docs/core/slot-engine-vs-stake-math-sdk) provides an in-depth comparison to help you decide. ## Installation & Setup Get started creating your first game using the `@slot-engine/core` library. ### Install package from npm Set up your Node.js project and install `@slot-engine/core`. **Using TypeScript instead of JavaScript is highly recommended.** **Slot Engine requires Node >= 23.8.0 or >= 22.15.0** to make use of native Zstandard compression features. npm pnpm yarn bun ```bash npm i @slot-engine/core ``` ```bash pnpm i @slot-engine/core ``` ```bash yarn add @slot-engine/core ``` ```bash bun install @slot-engine/core ``` ### Configure your game This example provides a quick overview to get you started. For detailed configuration options, see the [configuration guide](/docs/core/config). ```ts lineNumbers title="index.ts" import { defineUserState, defineSymbols, defineGameModes, InferGameType, createSlotGame, } from "@slot-engine/core" export const userState = defineUserState({ /* ... */ }) export type UserStateType = typeof userState export const symbols = defineSymbols({ /* ... */ }) export type SymbolsType = typeof symbols export const gameModes = defineGameModes({ /* ... */ }) export type GameModesType = typeof gameModes export type GameType = InferGameType export const game = createSlotGame({ id: "my-game", name: "My Game", maxWinX: 5000, scatterToFreespins: {}, gameModes, symbols, userState, hooks: {}, }) ``` ### Simulate your game ```ts lineNumbers=30 title="index.ts" game.configureSimulation({ simRunsAmount: { base: 100000, bonus: 100000, }, concurrency: 16, }) game.runTasks({ doSimulation: true, }) ``` Upon running your code, this will generate JSONL and CSV files containing the results of your simulated spins. **Currently, the output format is designed for compatibility with Stake Engine.** Future versions may support additional platforms as they emerge. Output customization is not available at this time. [Learn more](/docs/core/game-tasks/simulation) about game simulations. Before configuring your game, familiarize yourself with the [concepts and ideas](/docs/core/core-concepts) of the Core library to understand important terminology and background information. If you've used the Stake Math SDK before, many concepts will be familiar. # Comparing to Stake Math SDK (/docs/core/slot-engine-vs-stake-math-sdk) *** ## Slot Engine is inspired by Stake's Math SDK Slot Engine Core and Stake's Math SDK share many similarities, including comparable APIs. Originally conceived as a TypeScript port of the Math SDK, Slot Engine evolved during development to deliver an improved developer experience and enhanced code versioning capabilities. ## Challenges & Solutions Here's how Slot Engine addresses key challenges encountered with the Math SDK: ### Stake Math SDK Challenges Stake's solution provides a Python codebase that developers can clone and customize. However, game development with the Math SDK presents several challenges: #### Complexity Barriers * Requires deep understanding of complex inner workings * Features deep class inheritance and functions spanning multiple files * Can overwhelm developers, especially those new to programming #### Type Safety Issues * Lacks comprehensive type definitions * Results in `any` types throughout the codebase * Complicates development and debugging #### Version Management Problems * Not distributed through package managers * Upgrading existing games becomes cumbersome and risky * Custom code modifications make updates nearly impossible without significant refactoring Despite these challenges, the Stake Math SDK is battle-tested and widely used by industry professionals for creating sophisticated games. ### Slot Engine's Approach Slot Engine improves the developer experience by reducing some complexity through design choices: #### Simplified Architecture * Consciously avoids exposing all source code during development * Provides utility functions, services, and hooks instead * Reduces the need to override core functionality #### Better Developer Experience * Lower learning curve for new developers * Cleaner, more maintainable code structure * Improved type safety #### Package Distribution * Distributed as an npm package for easy installation and updates * Semantic versioning ensures predictable upgrade paths * No need to clone and modify source code directly ## Fundamental Differences ### "Magic Methods" Examining Stake's example games reveals that developers must manually invoke methods like `reset_seed()`, `reset_book()`, or `check_repeat()`. The purpose of these methods isn't immediately clear—understanding their role in game simulation requires diving deep into the underlying implementation. Slot Engine eliminates most "magic methods" by incorporating their functionality directly into the core logic, reducing developer confusion and improving code clarity for developers. ### Game Flow *(This text explains some opinions of the author and the reason for certain design decisions)* Stake's Math SDK provides pre-built functions for common game mechanics. However, these functions are designed for standard game patterns and often fall short when implementing unique features. For instance, the default functions cannot accommodate super bonus mechanics without modification. Since every slot game has distinctive requirements, developers frequently need to override entire functions to achieve their desired functionality. Slot Engine takes a fundamentally different approach to address this. Rather than offering monolithic functions for major game flow components, the Core library provides granular utility functions that serve as building blocks for custom game logic. Developers implement their complete game flow within a single hook function that integrates with the game configuration. This hook orchestrates the entire game sequence using utility functions combined with custom code. While this approach may initially appear more complex than navigating pre-built functions, it offers significant advantages: it eliminates the need to navigate complex source code hierarchies and override multiple interconnected functions, ultimately providing developers with greater flexibility to create truly customized gaming experiences. # What is Slot Engine (/docs/core/what-is-slot-engine) *** ## Introduction Slot Engine is a comprehensive TypeScript library ecosystem for building slot games. It provides robust mathematical and game logic foundations, with client visualization capabilities planned for future releases. The term "Slot Engine" refers to both the complete ecosystem and the core library specifically. ## Why Slot Engine The launch of Stake Engine in 2025 introduced a platform where developers can publish games to the world's largest casino, bringing a new wave of developers and studios into the space. While Stake provides a purpose-built SDK, it has limitations discussed [on this page](/docs/core/slot-engine-vs-stake-math-sdk). Since Stake's math SDK is written in Python, many developers—particularly those with web development backgrounds in TypeScript or JavaScript— face barriers to entry when creating games. **Slot Engine provides TypeScript and JavaScript developers with an alternative for creating slot games.** # Custom Game State (/docs/core/config/custom-state) *** ## State in Slot Engine The game state holds information about the current simulation, such as the current simulation ID or whether free spins were triggered. By design, the game state includes only essential game flow properties to reduce bloat. When developers need to track additional information—such as whether a specific feature was triggered— they should define custom state properties. ## Defining custom State To define additional state, use the `defineUserState` function and pass the resulting object to your game configuration. For example: ```ts lineNumbers import { defineUserState, createSlotGame, InferGameType } from "@slot-engine/core" export const userState = defineUserState({ triggeredSuperFreespins: false, freespinsUpgradedToSuper: false, globalMultiplier: 1 }) export type UserStateType = typeof userState export type GameType = InferGameType export const game = createSlotGame({ /* the rest of your configuration */ userState, }) ``` The values defined here will be set as the initial values for each state property on every new simulation. ## Using custom State In your [game implementation](/docs/core/game-implementation), you will have access to the underlying state via the context. From there you can access your custom state. ```ts lineNumbers export function onHandleGameFlow(ctx: GameContext) { /* the rest of your game flow */ // Example usage if (ctx.state.userData.triggeredSuperFreespins) { ctx.state.userData.globalMultiplier = 10 } } ``` # Game Modes (/docs/core/config/game-modes) *** ## Introduction A game mode defines a **purchasable** game behavior, similar to "bet modes" in the Stake Math SDK. Game modes can vary in cost, outcomes, symbols, and mechanics. Common game mode patterns include: * **Base game** - The standard gameplay at 1x bet multiplier * **Ante Bet** - Increased odds of hitting free spins for extra cost * **Bonus game** - e.g. 100x bet for instant free spins * **Super bonus** - e.g. 500x bet for instant super free spins Players can bet on the base game or purchase bonus features directly. A "base" mode can still include free spins triggered naturally, while a dedicated "bonus" mode allows immediate access for an extra cost. ## Defining Game Modes Game modes usually require a lot of configuration. They define which symbols make up your reel strips and which simulation results to output. Here is a relatively simple example with one game mode: ```ts lineNumbers import { defineGameModes, GameMode } from "@slot-engine/core" export const gameModes = defineGameModes({ base: new GameMode({ name: "base", cost: 1, rtp: 0.96, reelsAmount: 5, symbolsPerReel: [3, 3, 3, 3, 3], isBonusBuy: false, reelSets: [ /* list of reel sets */ ], resultSets: [ /* list of result sets */ ], }), }) ``` ## GameMode Constructor Options All options are required. | Property | Type | Description | | ---------------- | ------------- | ---------------------------------------------------------------------- | | `name` | `string` | Name of the game mode. Must match the key as set in `defineGameModes`. | | `reelsAmount` | `number` | Number of reels the board has. | | `symbolsPerReel` | `number[]` | Amount of symbols on each reel. | | `cost` | `number` | Cost of the game mode, multiplied by the base bet. | | `rtp` | `number` | The target RTP of the game mode, between 94 and 98. | | `reelSets` | `ReelSet[]` | See: [Game Modes: Reel Sets](/docs/core/config/reel-sets) | | `resultSets` | `ResultSet[]` | See: [Game Modes: Result Sets](/docs/core/config/result-sets) | | `isBonusBuy` | `boolean` | Whether this game mode is a bonus buy. | # Hooks (/docs/core/config/hooks) *** ## Introduction Hooks are a way of adding your own code on top of Slot Engine. They allow you to execute code or update state at specific points of the program. ## List of Hooks These are the hooks you can define in your game configuration. ### onHandleGameFlow | Property | Type | Required | | ------------------ | ---------------------------- | -------- | | `onHandleGameFlow` | `(ctx: GameContext) => void` | yes | The complete game logic must be [implemented](/docs/core/game-implementation) in this hook. ### onSimulationAccepted | Property | Type | Required | | ---------------------- | ---------------------------- | -------- | | `onSimulationAccepted` | `(ctx: GameContext) => void` | | This hook is called after a simulation is accepted and the payout has been written to the book. Useful to run code after completing a simulation and before continuing with the next. **No payouts or results should be altered using this hook!** But adding additional data with `ctx.services.data.record()` is perfectly fine. # Overview (/docs/core/config) *** ## Setting up your game Create a game by calling `createSlotGame()` and passing a configuration object. Begin by defining basic game details, then explore the comprehensive [configuration options](#options) detailed below. ```ts lineNumbers import { createSlotGame, SPIN_TYPE } from "@slot-engine/core" export const game = createSlotGame({ id: "my-game", name: "My Game", maxWinX: 5000, padSymbols: 1, scatterToFreespins: { [SPIN_TYPE.BASE_GAME]: { 3: 10, 4: 12, 5: 15, }, [SPIN_TYPE.FREE_SPINS]: { 3: 6, 4: 8, 5: 10, }, }, symbols: {}, gameModes: {}, userState: {}, hooks: { onHandleGameFlow(ctx) {}, }, }) ``` When using TypeScript, you will encounter a type error. This is expected because `createSlotGame` requires a type argument. Let's create a type for your game. ```ts lineNumbers import { createSlotGame, SPIN_TYPE } from "@slot-engine/core" // [!code --] import { createSlotGame, SPIN_TYPE, InferGameType } from "@slot-engine/core" // [!code ++] export type GameType = InferGameType // [!code ++] export const game = createSlotGame({}) // [!code --] export const game = createSlotGame({}) // [!code ++] ``` The utility type `InferGameType` requires three type arguments: game modes, symbols, and state. Since we haven't configured these yet, you can use `any` for now. You can now prepare the rest of the configuration. ```ts lineNumbers import { createSlotGame, GameConfig, InferGameType } from "@slot-engine/core" // [!code --] import { createSlotGame, // [!code ++] SPIN_TYPE, // [!code ++] InferGameType, // [!code ++] defineGameModes, // [!code ++] defineSymbols, // [!code ++] defineUserState, // [!code ++] } from "@slot-engine/core" // [!code ++] export const gameModes = defineGameModes({}) // [!code ++] export type GameModesType = typeof gameModes // [!code ++] export const symbols = defineSymbols({}) // [!code ++] export type SymbolsType = typeof symbols // [!code ++] export const userState = defineUserState({}) // [!code ++] export type UserStateType = typeof userState // [!code ++] export type GameType = InferGameType // [!code --] export type GameType = InferGameType // [!code ++] export const game = createSlotGame({ /* the rest of your configuration */ symbols: {}, // [!code --] gameModes: {}, // [!code --] userState: {}, // [!code --] symbols, // [!code ++] gameModes, // [!code ++] userState, // [!code ++] }) ``` You have now completed the basic game setup. Continue by configuring [symbols](/docs/core/config/symbols), [game modes](/docs/core/config/game-modes),and [state](/docs/core/config/custom-state). At the heart of your game lies the actual [implementation](/docs/core/game-implementation). Finally, [simulate](/docs/core/game-tasks/simulation) your game, tweak settings to improve the feel of it, and [optimize](/docs/core/game-tasks/optimization) results to achieve your desired RTP. ## createSlotGame() Options | Property | Type | Description | Required | | -------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `id` | `string` | A unique identifier for your game. | yes | | `name` | `string` | The name of your game. | yes | | `maxWinX` | `number` | The maximum bet multiplier payout. Wins exceeding this number will be capped. | yes | | `gameModes` | `Record` | See: [Game Configuration: Game Modes](/docs/core/config/game-modes) | yes | | `symbols` | `Record` | See: [Game Configuration: Symbols](/docs/core/config/symbols) | yes | | `padSymbols` | `number` | Amount of padding symbol rows above and below the active board.
Used to display partially visible symbols in the frontend at the top and bottom of the board.

Defaults to 1 | yes | | `scatterToFreespins` | `Record>` | A mapping from spin type to scatter counts to the number of free spins awarded. | yes | | `userState` | `Record` | See: [Game Configuration: User State](/docs/core/config/custom-state) | yes | | `hooks` | `Record unknown>` | See: [Game Configuration: Hooks](/docs/core/config/hooks) | yes | | `rootDir` | `string` | Normally you would `cd` into the game directory and run it from there. If you run your game from a different file or `process.cwd()` is not where your game lies, specify this option and provide a path. `__dirname` should work fine. | | # Reel Sets (/docs/core/config/reel-sets) *** ## Introduction A reel set is a list of reel strips, where each reel strip defines the symbols that can land on the board for that specific reel. Having a well-designed and balanced reel set is **crucial for achieving the desired gameplay experience** and feel of your slot game. For example, the last reel could hold less valuable symbols to make it even more exciting if you do hit a full line. Designing a balanced reel set can be hard without mathematical background knowledge and is usually done by professional mathematicians. ## Automatic Generation of Reel Sets Slot Engine simplifies reel set creation by providing a `GeneratedReelSet` class that builds a CSV file containing a reel set. Developers simply need to define weights for the symbols that should appear on the reels, and the generator will randomly distribute symbols according to those weights. **This is not a replacement for professional mathematicians** and should be considered a starting point. You will need to playtest your game to evaluate the gameplay experience and adjust your reel generator configuration accordingly or edit the generated reels manually. Add one or more reel sets to your game mode configuration. **Each game mode must specify at least one reel set**. ```ts lineNumbers import { defineGameModes, GameMode, GeneratedReelSet } from "@slot-engine/core" export const gameModes = defineGameModes({ base: new GameMode({ /* the rest of your configuration */ reelSets: [ new GeneratedReelSet({ id: "base", overrideExisting: false, symbolWeights: {}, }), ], }), }) ``` ## GeneratedReelSet Options ### General Options | Property | Type | Description | Required | | ------------------ | ------------------------ | ------------------------------------------------------ | -------- | | `id` | `string` | The unique identifier of the reel set / generator. | yes | | `symbolWeights` | `Record` | Mapping of symbol ID's to weights. | yes | | `overrideExisting` | `boolean` | If true, existing reels CSV files will be overwritten. | | | `rowsAmount` | `number` | The number of rows in the reelset.
Default: 250 | | | `seed` | `number` | Seed to change the RNG. | | ### Advanced Options All advanced options are optional. #### spaceBetweenSameSymbols \[!toc] ```ts number | Record ``` Prevent the same symbol from appearing directly above or below itself.\ This can be a single number to affect all symbols, or a mapping of symbol IDs to their respective spacing values. Must be 1 or higher, if set. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, spaceBetweenSameSymbols: 4, // or spaceBetweenSameSymbols: { S: 5, W: 3, }, }) ``` #### spaceBetweenSymbols \[!toc] ```ts Record> ``` Prevents specific symbols from appearing within a certain distance of each other. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, spaceBetweenSymbols: { S: { SS: 3, W: 1 }, // ^ Scatter is at least 3 away from super scatter and 1 away from wild }, }) ``` #### preferStackedSymbols \[!toc] ```ts number ``` A percentage value 0-100 that indicates the likelihood of a symbol being stacked. A value of 0 means no stacked symbols, while 100 means all symbols are stacked. This is only a preference. Symbols may still not be stacked if other restrictions (like `spaceBetweenSameSymbols`) prevent it. This setting is overridden by `symbolStacks`. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, preferStackedSymbols: 50, }) ``` #### symbolStacks \[!toc] ```ts Record< string, { chance: number | Record min?: number | Record max?: number | Record } > ``` A mapping of symbols to their respective advanced stacking configuration. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, symbolStacks: { W: { chance: { "1": 20, "2": 20, "3": 20, "4": 20 }, // 20% chance to be stacked on reels 2-5 min: 2, // At least 2 wilds in a stack max: 4, // At most 4 wilds in a stack }, }, }) ``` #### limitSymbolsToReels \[!toc] ```ts Record ``` Configures symbols to only appear on specific reels. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, limitSymbolsToReels: { S: [0, 2, 4], // Scatter only on reels 1, 3, 5. }, }) ``` #### symbolQuotas \[!toc] ```ts Record> ``` Defines minimum symbol quotas on reels. The quota (1-100%) defines how often a symbol should appear in the reelset, or in a specific reel. This is particularly useful for controlling the frequency of special symbols like scatters or wilds. Reels not provided for a symbol will use the weights from `symbolWeights`. *Any* small quota will ensure that the symbol appears at least once on the reel. ```ts new GeneratedReelSet({ id: "example", symbolWeights: {}, symbolQuotas: { S: 3, // 3% of symbols on each reel will be scatters W: { "1": 10, "2": 5, "3": 3, "4": 1 }, // Wilds will appear with different quotas on selected reels }, }) ``` ## Manual Reel Set Creation You can define reel sets manually using the `StaticReelSet` class, where the reels are defined either via a JSON array or via a CSV file. ### CSV File Reel Sets For a game mode with 5 reels, the content of a CSV reel set file must have a structure like below. ```csv L4,L5,L1,L5,L4 L2,L4,L4,L3,L3 L4,H2,W,L4,L1 L3,L3,H4,H4,H1 H1,H4,H1,L5,H2 L5,L4,L3,S,S ... ``` Then add your reel set to a game mode like so: ```ts lineNumbers import { defineGameModes, GameMode, StaticReelSet } from "@slot-engine/core" import path from "path" export const gameModes = defineGameModes({ base: new GameMode({ /* the rest of your configuration */ reelSets: [ new StaticReelSet({ id: "base", csvPath: path.join(__dirname, "path-to-your", "reelset.csv"), }), ], }), }) ``` ### JSON Reel Sets You can also define your reel strips as a JSON array of symbol ID's like below. Note that this configuration is read from left to right, top to bottom, and *not* top to bottom, left to right like in CSV reels. ```ts lineNumbers import { defineGameModes, GameMode, StaticReelSet } from "@slot-engine/core" export const gameModes = defineGameModes({ base: new GameMode({ /* the rest of your configuration */ reelSets: [ new StaticReelSet({ id: "base", reels: [ ["L4", "L2", "L4", "L3", "H1", "L5"], ["L5", "L4", "H2", "L3", "H4", "L4"], ["L1", "L4", "W", "H4", "H1", "L3"], ["L5", "L3", "L4", "H4", "L5", "S"], ["L4", "L3", "L1", "H1", "H2", "S"], ] }), ], }), }) ``` # Result Sets (/docs/core/config/result-sets) *** ## Introduction Slot games have thousands if not millions of possible outcomes—each spin is different. A player shouldn't see the exact same outcome twice. With result sets you **ensure diverse simulation results**. You want to ensure there are plenty of zero-win scenarios as well as many different max win simulations— and everything in between. Before a simulation run starts, each simulation is assigned one of the specified result sets based on the total quota of all result sets for a game mode, and must fulfill its criteria. Only when a **simulation meets the expected result set criteria** does the program proceed to the next simulation. Result sets commonly cover the following scenarios: * Zero-win spins * Win spins * Free spins * Max wins But developers can also specify very detailed and rare criteria: * Free spins upgraded to super bonus resulting in max win **Result sets are a crucial part** of simulating a game. Depending on the configuration and implementation of a game, outcomes like max wins may be extremely rare and possibly don't occur naturally during, for example, 1,000,000 simulations. With result sets, the program forces such outcomes and retries until the given criteria are met. The more unlikely a scenario is to happen, the longer a simulation run may take. ## Defining Result Sets for a Game Mode The following example shows the definition of 4 common result sets. This setup ensures that a large portion of all simulations are either zero-win spins or normal base game hits. A smaller quota is dedicated to generating free spins simulations and a very small portion ensures enough max win simulations. ```ts lineNumbers export const gameModes = defineGameModes({ base: new GameMode({ name: "base", cost: 1, rtp: 0.96, /* the rest of the configuration */ resultSets: [ new ResultSet({ criteria: "0", quota: 0.4, multiplier: 0, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 1 }, }, }), new ResultSet({ criteria: "basegame", quota: 0.4, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 1 }, }, }), new ResultSet({ criteria: "freespins", quota: 0.1, forceFreespins: true, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 3, bonus2: 1 }, }, }), new ResultSet({ criteria: "maxwin", quota: 0.005, forceMaxWin: true, forceFreespins: true, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 1, bonus2: 3 }, evaluate: maxwinReelsEvaluation, }, }), ], }), }) ``` Keep in mind that you'll simulate at least 100.000 spins. If you have a max win quota of 0.005 and `(quota / totalQuota) * simsAmount` = `(0.005 / 1) * 100.000` = `500`, that's perfectly enough variety in max win simulations. ## ResultSet Options ### criteria \[!toc] | Property | Type | Required | | ---------- | -------- | -------- | | `criteria` | `string` | yes | A unique descriptive identifier for the result set. ### quota \[!toc] | Property | Type | Required | | -------- | -------- | -------- | | `quota` | `number` | yes | The quota of simulations that should fall under this result set. #### Example \[!toc] Let's say you have these 3 result sets: * "basegame": 90 quota * "freespins": 9 quota * "maxwin": 1 quota The total quota here is 100 (but can be any number, doesn't have to be 100). This means that, for example, every 100th simulation will result in a max win. **When using** [optimization](/docs/core/game-tasks/optimization), the quota **does not represent the actual frequency** of specific outcomes when playing, because the optimizer will redistribute the weights of all simulations. ### multiplier \[!toc] | Property | Type | Required | | ------------ | -------- | -------- | | `multiplier` | `number` | | The exact payout multiplier to be hit. ### forceMaxWin \[!toc] | Property | Type | Required | | ------------- | --------- | -------- | | `forceMaxWin` | `boolean` | | Whether a max win should be simulated. ### forceFreespins \[!toc] | Property | Type | Required | | ---------------- | --------- | -------- | | `forceFreespins` | `boolean` | | Whether free spins should be simulated. Setting `forceFreespins` alone won't do anything. It's up to you to include logic in your game implementation to check whether `ctx.state.currentResultSet.forceFreespins` is `true` and draw the board accordingly. ### userData \[!toc] | Property | Type | Required | | ---------- | --------------------- | -------- | | `userData` | `Record` | | Additional data that can be used in a custom evaluation function or in your game loop. ### evaluate \[!toc] | Property | Type | Required | | ---------- | ------------------------------- | -------- | | `evaluate` | `(ctx: GameContext) => boolean` | | Inject **custom checks** into the simulation evaluation logic. Use this to check for free spins that upgraded to super free spins or **any other arbitrary simulation criteria** not supported in the core. For example, you can use the `userData` from the result set to handle different game behavior. Then in your game implementation, set the state accordingly to ensure the simulation passes the criteria. ResultSet Evaluation Function Game Implementation ```ts lineNumbers new ResultSet({ criteria: "freespinsUpgradeToSuper", quota: 0.01, forceFreespins: true, reelWeights: { /* ... */ }, userData: { upgradeFreespins: true }, // [!code highlight] evaluate: freeSpinsUpgradeEvaluation, // [!code highlight] }), ``` ```ts lineNumbers export function freeSpinsUpgradeEvaluation(ctx: GameContext) { if (ctx.state.currentSpinType === SPIN_TYPE.BASE_GAME) return false return ctx.state.userData.freespinsUpgradedToSuper } ``` ```ts lineNumbers // Somewhere in your game implementation... // ResultSet `userData` can be used to achieve desired outcome. if ( ctx.state.currentSpinType == SPIN_TYPE.FREE_SPINS && ctx.state.currentResultSet.userData?.upgradeFreespins && !ctx.state.userData.freespinsUpgradedToSuper ) { // Upgrade FS here ctx.state.userData.freespinsUpgradedToSuper = true } ``` ### reelWeights \[!toc] | Property | Type | Required | | ------------- | ----------------------- | -------- | | `reelWeights` | `Record` | yes | Configure the weights of the reels in this ResultSet. If you need to support dynamic / special reel weights based on the simulation context, you can provide an `evaluate` function that returns the desired weights. If the `evaluate` function returns a falsy value, the usual spin type based weights will be used. #### Example 1 \[!toc] During the first spin of a "freespins" simulation (when scatters trigger FS), the reels with the ID `base1` will always be used to draw the board. During free spins, one of two possible reels will be picked by doing a weighted draw (`bonus1`: 75%, `bonus2`: 25%). ```ts lineNumbers new ResultSet({ criteria: "freespins", quota: 1, forceFreespins: true, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 3, bonus2: 1 }, }, }), ``` #### Example 2 \[!toc] Similar to the first example, but the `evaluate` function runs first and tells the program to use the `superbonus` reels after super free spins have been triggered. ```ts lineNumbers new ResultSet({ // Add your custom state as a type parameter ... criteria: "superFreespins", quota: 0.01, forceFreespins: true, reelWeights: { [SPIN_TYPE.BASE_GAME]: { base1: 1 }, [SPIN_TYPE.FREE_SPINS]: { bonus1: 3, bonus2: 1 }, evaluate: (ctx) => { if (ctx.state.userData.triggeredSuperFreespins) { // ... for type-safety! return { superbonus: 1 } } }, }, userData: { forceSuperFreespins: true }, }), ``` # Symbols (/docs/core/config/symbols) *** ## Defining Symbols for your Game Use the `defineSymbols` function to configure symbols. The function expects a configuration object where each key serves as the unique identifier for a symbol. Symbols are constructed using the `GameSymbol` class. You can define as many symbols as needed, provided each has a unique key and ID. ```ts lineNumbers import { defineSymbols, GameSymbol } from "@slot-engine/core" export const symbols = defineSymbols({ S: new GameSymbol({ id: "S", properties: { isScatter: true, }, }), W: new GameSymbol({ id: "W", properties: { isWild: true, }, }), H1: new GameSymbol({ id: "H1", pays: { 3: 10, 4: 75, 5: 250, }, }), }) ``` ## Paying Symbols To define payout amounts for symbols, set the `pays` property where the key is the number of matching symbols and the value is the bet multiplier to be paid out. ```ts lineNumbers export const symbols = defineSymbols({ H1: new GameSymbol({ id: "H1", pays: { 3: 10, 4: 75, 5: 250, }, }), L5: new GameSymbol({ id: "L5", pays: { 3: 0.2, 4: 0.4, 5: 0.8, }, }), }) ``` Payouts with **two or more decimal places are not recommended**, as this could result in payouts less than a cent.
**E.g. 0.10 bet × 0.75 multiplier = 0.075 payout**
Ensure the minimum bet will be 0.2 €/$ if you want to go this route.
## Special Symbols To define scatters, wilds, or other special symbols, set custom `properties` for your symbol. Multiple symbols can share the same properties. This is useful when you need multiple types of the same symbol category, such as different scatter variants. Properties help identify and count specific symbols on the board during gameplay. ```ts lineNumbers export const symbols = defineSymbols({ S: new GameSymbol({ id: "S", properties: { isScatter: true, }, }), SS: new GameSymbol({ id: "SS", properties: { isScatter: true, isSuperScatter: true, }, }), W: new GameSymbol({ id: "W", properties: { isWild: true, }, }), EW: new GameSymbol({ id: "EW", properties: { isWild: true, isExpandingWild: true, }, }), }) ``` ## Multipliers Symbol properties can hold any value, not just booleans. For example, you could specify a multiplier for a symbol. ```ts lineNumbers export const symbols = defineSymbols({ W: new GameSymbol({ id: "W", properties: { isWild: true, multiplier: 10, }, }), }) ``` However, in this case, the wild symbol will always have the specified multiplier when placed on the board. It's better to set `multiplier: 0` or **omit it** entirely. Symbol properties like multipliers **should be modified during game flow** for greater flexibility and clarity. If your game only requires static multipliers (e.g., a consistent 2x wild multiplier), the above approach is perfectly acceptable. ## Compare Symbols Sometimes you might need to compare two symbols. Each `GameSymbol` instance has a `compare()` method for this purpose. ### Compare by Symbol When passing a `GameSymbol` to the `compare` function, only the symbol ID's are compared. ```ts const wild = ctx.config.symbols.get("W")! const scatter = ctx.config.symbols.get("S")! console.log(scatter.compare(wild)) // false ``` ### Compare by Symbol Properties When comparing by properties, the result is `true` **when all properties and their values match**. ```ts const result = someSymbol.compare({ someProperty: true, anotherProperty: 10 }) ``` # Board Service (/docs/core/game-context/board-service) *** ## Methods ### getBoardReels() | Method | Type | | ------------------------------------ | ------------- | | `ctx.services.board.getBoardReels()` | `() => Reels` | Returns the active board reels. ### getPaddingTop() | Method | Type | | ------------------------------------ | ------------- | | `ctx.services.board.getPaddingTop()` | `() => Reels` | Returns the top padding reels. ### getPaddingBottom() | Method | Type | | --------------------------------------- | ------------- | | `ctx.services.board.getPaddingBottom()` | `() => Reels` | Returns the bottom padding reels. ### getSymbol() | Method | Type | | -------------------------------- | ------------------------------------- | | `ctx.services.board.getSymbol()` | `(reelIndex, rowIndex) => GameSymbol` | #### Parameters \[!toc] | Parameter | Type | | ----------- | -------- | | `reelIndex` | `number` | | `rowIndex` | `number` | Returns the symbol at the specified position. ### setSymbol() | Method | Type | | -------------------------------- | --------------------------------------- | | `ctx.services.board.setSymbol()` | `(reelIndex, rowIndex, symbol) => void` | #### Parameters \[!toc] | Parameter | Type | | ----------- | ------------ | | `reelIndex` | `number` | | `rowIndex` | `number` | | `symbol` | `GameSymbol` | Sets the symbol at the specified position. ### removeSymbol() | Method | Type | | ----------------------------------- | ---------------------------------- | | `ctx.services.board.removeSymbol()` | `(reelIndex, rowIndex) => boolean` | #### Parameters \[!toc] | Parameter | Type | | ----------- | -------- | | `reelIndex` | `number` | | `rowIndex` | `number` | Removes the symbol from the board at the specified position. Returns a boolean, whether the symbol was removed. ### setSymbolsPerReel() | Method | Type | | ---------------------------------------- | -------------------------- | | `ctx.services.board.setSymbolsPerReel()` | `(symbolsPerReel) => void` | #### Parameters \[!toc] | Parameter | Type | | ---------------- | ---------- | | `symbolsPerReel` | `number[]` | Sets a new temporary value for `symbolsPerReel`. The value will be persisted until changed by the user or until the simulation is reset or the next simulation starts. ### setReelsAmount() | Method | Type | | ------------------------------------- | ----------------------- | | `ctx.services.board.setReelsAmount()` | `(reelsAmount) => void` | #### Parameters \[!toc] | Parameter | Type | | ------------- | -------- | | `reelsAmount` | `number` | Sets a new temporary value for `reelsAmount`. The value will be persisted until changed by the user or until the simulation is reset or the next simulation starts. ### resetBoard() | Method | Type | | --------------------------------- | ------------ | | `ctx.services.board.resetBoard()` | `() => void` | Resets and clears the board. ### getAnticipation() | Method | Type | | -------------------------------------- | ----------------- | | `ctx.services.board.getAnticipation()` | `() => boolean[]` | Array of booleans representing anticipation for reels. Anticipation is a visual effect that usually teases potential free spins when one more scatter is needed to trigger it. For example, an anticipation array of `[false, false, false, true, true]` can instruct the client to apply anticipation effects to reels 4 and 5. ### setAnticipationForReel() | Method | Type | | --------------------------------------------- | ---------------------------- | | `ctx.services.board.setAnticipationForReel()` | `(reelIndex, value) => void` | #### Parameters \[!toc] | Parameter | Type | | ----------- | --------- | | `reelIndex` | `number` | | `value` | `boolean` | Sets anticipation state for a board reel. ### countSymbolsOnReel() | Method | Type | | ----------------------------------------- | ------------------------------------------- | | `ctx.services.board.countSymbolsOnReel()` | `(symbolOrProperties, reelIndex) => number` | #### Parameters \[!toc] | Parameter | Type | | -------------------- | ----------------------------------- | | `symbolOrProperties` | `GameSymbol \| Record` | | `reelIndex` | `number` | Counts the symbols on the specified reel. ### countSymbolsOnBoard() | Method | Type | | ------------------------------------------ | ---------------------------------------------------------- | | `ctx.services.board.countSymbolsOnBoard()` | `(symbolOrProperties) => [number, Record]` | #### Parameters \[!toc] | Parameter | Type | | -------------------- | ----------------------------------- | | `symbolOrProperties` | `GameSymbol \| Record` | Counts how many symbols matching the criteria are on the board. Returns a tuple where the first element is the total count, and the second element is a record of counts per reel index. ### isSymbolOnAnyReelMultipleTimes() | Method | Type | | ----------------------------------------------------- | --------------------- | | `ctx.services.board.isSymbolOnAnyReelMultipleTimes()` | `(symbol) => boolean` | #### Parameters \[!toc] | Parameter | Type | | --------- | ------------ | | `symbol` | `GameSymbol` | Checks if the given symbol occurrs multiple times on any reel. ### getReelStopsForSymbol() | Method | Type | | -------------------------------------------- | ------------------------------- | | `ctx.services.board.getReelStopsForSymbol()` | `(reels, symbol) => number[][]` | #### Parameters \[!toc] | Parameter | Type | | --------- | ------------ | | `reels` | `Reels` | | `symbol` | `GameSymbol` | Returns all positions of a symbol in a reel set. Useful to retrieve all possible scatter positions, for example. ### combineReelStops() | Method | Type | | --------------------------------------- | ------------------------------ | | `ctx.services.board.combineReelStops()` | `(...reelStops) => number[][]` | #### Parameters \[!toc] | Parameter | Type | | ----------- | -------------- | | `reelStops` | `number[][][]` | Combines multiple arrays of reel stops into a single array of reel stops. If you had multiple scatter variants, you could use this to get a single array of all scatter positions in a reel set. #### Usage \[!toc] ```ts const reels = ctx.services.board.getRandomReelset() const scatter = config.symbols.get("S")! const superScatter = config.symbols.get("SS")! const reelStops = ctx.services.board.combineReelStops( ctx.services.board.getReelStopsForSymbol(reels, scatter), ctx.services.board.getReelStopsForSymbol(reels, superScatter), ) ``` ### getRandomReelStops() | Method | Type | | ----------------------------------------- | ------------------------------------------------------ | | `ctx.services.board.getRandomReelStops()` | `(reels, reelStops, amount) => Record` | #### Parameters \[!toc] | Parameter | Type | | ----------- | ------------ | | `reels` | `Reels` | | `reelStops` | `number[][]` | | `amount` | `number` | From a list of reel stops on reels, selects a random stop for `amount` number of reels. This is mostly useful to forcibly place scatters on the board. #### Usage \[!toc] ```ts const reels = ctx.services.board.getRandomReelset() const scatter = config.symbols.get("S")! const reelStops = ctx.services.board.getReelStopsForSymbol(reels, scatter) const scatterReelStops = ctx.services.board.getRandomReelStops(reels, reelStops, 3) ctx.services.board.drawBoardWithForcedStops({ reels, forcedStops: scatterReelStops, }) ``` ### getRandomReelset() | Method | Type | | --------------------------------------- | ------------- | | `ctx.services.board.getRandomReelset()` | `() => Reels` | Selects a random reel set based on the configured weights of the current result set. ### drawBoardWithForcedStops() | Method | Type | | ----------------------------------------------- | ------------------------------------------------------ | | `ctx.services.board.drawBoardWithForcedStops()` | `(opts: { reels, forcedStops, randomOffset }) => void` | #### Parameters \[!toc] | Parameter | Type | Description | Required | | ------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `opts.reels` | `Reels` | | yes | | `opts.forcedStops` | `Record` | | yes | | `opts.randomOffset` | `boolean` | Whether to apply a random offset to the stops. Adds a bit of randomization to where exactly your forced symbol lands on the reel.
Default: true | | Draws a board using specified reel stops. #### Usage \[!toc] ```ts const reels = ctx.services.board.getRandomReelset() const scatter = config.symbols.get("S")! const reelStops = ctx.services.board.getReelStopsForSymbol(reels, scatter) const scatterReelStops = ctx.services.board.getRandomReelStops(reels, reelStops, 3) ctx.services.board.drawBoardWithForcedStops({ reels, forcedStops: scatterReelStops, }) ``` ### drawBoardWithRandomStops() | Method | Type | | ----------------------------------------------- | ----------------- | | `ctx.services.board.drawBoardWithRandomStops()` | `(reels) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | ------- | | `reels` | `Reels` | Draws a board using random reel stops. ### tumbleBoard() | Method | Type | | ---------------------------------- | ---------------------------------------------------------------- | | `ctx.services.board.tumbleBoard()` | `(symbolsToDelete) => { newBoardSymbols, newPaddingTopSymbols }` | #### Parameters \[!toc] | Parameter | Type | | ----------------- | -------------------------------------------- | | `symbolsToDelete` | `Array<{ reelIdx: number; rowIdx: number }>` | #### Return Object \[!toc] | Parameter | Type | | ---------------------- | ------------------------------ | | `newBoardSymbols` | `Record` | | `newPaddingTopSymbols` | `Record` | Tumbles the board. All given symbols will be deleted and new symbols will fall from the top. If you use the symbols from `winCombinations` returned by a win type, ensure symbols are deduped to prevent bugs during tumbling. For example, the same Wild symbol may be associated with multiple win combinations. To dedupe, use `ctx.services.game.dedupeWinSymbols()`. The resulting symbol positions are safe to use for tumbling. The function returns the new symbols that were added to each reel. For example, `newBoardSymbols` will be an object where the keys are the reel indexes and the values are the added symbols for that reel. ### tumbleBoardAndForget() - EXPERIMENTAL This method is experimental and may be changed or replaced in the future. If you plan to use this method, ensure `padSymbols` is set to `0` in the game config or you will most likely experience bugs or unexpected behavior. | Method | Type | | ------------------------------------------- | ----------------------------------------------------- | | `ctx.services.board.tumbleBoardAndForget()` | `(opts) => { newBoardSymbols, newPaddingTopSymbols }` | #### Parameters \[!toc] | Parameter | Type | | ---------------------- | -------------------------------------------- | | `opts.symbolsToDelete` | `Array<{ reelIdx: number; rowIdx: number }>` | | `opts.reels` | `Reels` | | `opts.forcedStops` | `number[]` | #### Return Object \[!toc] | Parameter | Type | | ---------------------- | ------------------------------ | | `newBoardSymbols` | `Record` | | `newPaddingTopSymbols` | `Record` | Tumbles the board similar to `tumbleBoard()`. While `tumbleBoard()` remembers the last tumble and can seamlessly tumble multiple times in a row, `tumbleBoardAndForget()` will not remember what it did. **This is useful to do a single one-off tumble.** While `tumbleBoard()` always uses the last used reel set that the board was drawn with, `tumbleBoardAndForget()` can tumble symbols from an arbitrary reel set and won't override any internal board state (besides the symbols on the reels). This can be particularly useful if you need to fill a part of the board with some blocker symbols (think of the game Templar Tumble, or Temple Tumble). If you plan to use this method, ensure `padSymbols` is set to `0` in the game config or you will most likely experience bugs or unexpected behavior. # Config (/docs/core/game-context/config) *** ## Introduction The static game configuration object provides access to various game-specific properties as defined in your `createSlotGame` function. The game config **must not be modified** during runtime. ## Properties Only properties that are relevant for game implementation are listed here. | Property | Type | Description | | ---------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anticipationTriggers` | `Record` | A mapping of spin types to the number of scatter symbols required to trigger anticipation. | | `maxWinX` | `number` | The maximum bet multiplier payout. Wins exceeding this number will be capped. | | `padSymbols` | `number` | Amount of padding symbol rows above and below the active board.
Used to display partially visible symbols in the frontend at the top and bottom of the board.

Defaults to 1 | | `symbols` | `Map` | See: [Game Configuration: Symbols](/docs/core/config/symbols) | | `scatterToFreespins` | `Record>` | A mapping from spin type to scatter counts to the number of free spins awarded. | Any other properties you might find under `ctx.config` shouldn't be used as they only serve internal purposes. They might be made inaccessible in a future update. # Data Service (/docs/core/game-context/data-service) *** ## Introduction Slot Engine has data layers that store JSON data required for game optimization, statistical analysis and frontend visualization. ### Book Each simulation produces a "book" that contains information about the simulation, like simulation ID, payout values, and JSON data for frontend visualization. The book stores **events** as an array of objects with an index, type and arbitrary data. Events define, in chronological order, **what should be displayed to the player** on the frontend. Common events include: * Revealing the board symbols * Displaying win lines * Displaying win amount * Trigger free spins screen In short: (Almost) every visual change on the frontend for a single spin is defined inside a book. For example: ```ts lineNumbers ctx.services.data.addBookEvent({ type: "show-winlines", data: { lines: [0, 3] wins: [ [ { reel: 0, row: 0 }, { reel: 1, row: 0 }, { reel: 2, row: 0 }, ], [ { reel: 0, row: 2 }, { reel: 1, row: 2 }, { reel: 2, row: 2 }, ], ] } }) ctx.services.data.addBookEvent({ type: "add-update-balance", data: { value: 200, } }) ``` Each added event receives an auto-incremented index, allowing the frontend to process events in the correct chronological order. ### Recorder The recorder allows grouping simulations by arbitrary criteria. If using [optimization](/docs/core/game-tasks/optimization), this would allow you to target specific simulations to optimize. It also enables capturing notable runs (e.g. max win simulations) so they can be replayed locally for debugging or demonstration. Records are written to the `__build__` directory as `force_record_.json`. The JSON structure matches the Stake Math SDK format for interoperability. Without having to configure anything, Slot Engine automatically groups simulations by their result set criteria. You can add additional records as needed using `record()`. ## Methods ### addBookEvent() | Method | Type | | ---------------------------------- | ----------------- | | `ctx.services.data.addBookEvent()` | `(event) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | --------------------------------------------- | | `event` | `{ type: string, data: Record }` | Adds a new event to the current book. ### record() | Method | Type | | ---------------------------- | ---------------- | | `ctx.services.data.record()` | `(data) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | --------------------------------------------- | | `data` | `Record` | Record data for statistical analysis. ### recordSymbolOccurrence() | Method | Type | | -------------------------------------------- | ---------------- | | `ctx.services.data.recordSymbolOccurrence()` | `(data) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | ----------------------------------------------------------------------------- | | `data` | `{ kind: number; symbolId: string; spinType: SpinType; [key: string]: any; }` | A wrapper around `ctx.services.data.record()` to record symbol occurrences. # Game Service (/docs/core/game-context/game-service) *** ## Methods ### getReelsetById() | Method | Type | | ------------------------------------ | ------------------------- | | `ctx.services.game.getReelsetById()` | `(gameMode, id) => Reels` | #### Parameters \[!toc] | Parameter | Type | | ---------- | -------- | | `gameMode` | `string` | | `id` | `string` | Retrieves a reel set by its ID within a specific game mode. ### getFreeSpinsForScatters() | Method | Type | | --------------------------------------------- | ------------------------------------ | | `ctx.services.game.getFreeSpinsForScatters()` | `(spinType, scatterCount) => number` | #### Parameters \[!toc] | Parameter | Type | | -------------- | ---------- | | `spinType` | `SpinType` | | `scatterCount` | `number` | Retrieves the number of free spins awarded for a given spin type and scatter count based on the `scatterToFreespins` configuration. ### getResultSetByCriteria() | Method | Type | | -------------------------------------------- | ------------------------------- | | `ctx.services.game.getResultSetByCriteria()` | `(mode, criteria) => ResultSet` | #### Parameters \[!toc] | Parameter | Type | | ---------- | -------- | | `mode` | `string` | | `criteria` | `string` | Retrieves a result set by its criteria within a specific game mode. ### getSymbolArray() | Method | Type | | ------------------------------------ | -------------------- | | `ctx.services.game.getSymbolArray()` | `() => GameSymbol[]` | Returns all symbols as an array. ### getCurrentGameMode() | Method | Type | | ---------------------------------------- | ---------------- | | `ctx.services.game.getCurrentGameMode()` | `() => GameMode` | Gets the configuration for the current [game mode](/docs/core/config/game-modes). ### verifyScatterCount() | Method | Type | | ---------------------------------------- | ------------------------- | | `ctx.services.game.verifyScatterCount()` | `(numScatters) => number` | #### Parameters \[!toc] | Parameter | Type | | ------------- | -------- | | `numScatters` | `number` | Ensures the requested number of scatters is valid based on the game configuration. Returns a valid number of scatters. ### awardFreespins() | Method | Type | | ------------------------------------ | ------------------ | | `ctx.services.game.awardFreespins()` | `(amount) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | -------- | | `amount` | `number` | Adds the given number of free spins to the state. ### dedupeWinSymbols() | Method | Type | | -------------------------------------- | ------------------------------------------ | | `ctx.services.game.dedupeWinSymbols()` | `(winCombinations) => { reelIdx, rowIdx }` | #### Parameters \[!toc] | Parameter | Type | | ----------------- | ------------------ | | `winCombinations` | `WinCombination[]` | #### Return Object \[!toc] | Parameter | Type | | --------- | -------- | | `reelIdx` | `number` | | `rowIdx` | `number` | When working with `winCombinations` returned from any win type, make sure you dedupe winning symbols. It may be possible for the same symbol (e.g. Wild) to be included in multiple win combinations. If you then wanted to tumble the board based on the winning symbols, that Wild symbol would be tumbled multiple times, which might lead to errors. Another example is games like "Sugar Rush", where winning combinations increase a multiplier on the board. You wouldn't want the same symbol to trigger a multiplier increase multiple times. # Context (/docs/core/game-context) *** ## Introduction The context (or game context) provides access to the underlying state. It also provides many utility functions that ease game implementation. The context is typically accessed via the `ctx` parameter in [hooks](/docs/core/config/hooks). ```ts lineNumbers type Context = GameContext export function onHandleGameFlow(ctx: Context) { const reels = ctx.services.board.getRandomReelset() ctx.services.board.drawBoardWithRandomStops(reels) // type safe! ⤵ const scatter = ctx.config.symbols.get("S")! const [count] = ctx.services.board.countSymbolsOnBoard(scatter) } ``` ## Properties ### config You can access the complete game configuration through the context. Keep in mind that you **shouldn't do any modifications to your game config** inside your game implementation. ```ts const config = ctx.config ``` [Further reading](/docs/core/game-context/config) ### services Services are cohesive function groups that encapsulate and manage specific parts of the game state. You will use them often when implementing your game. * [Game Service](/docs/core/game-context/game-service) * [Data Service](/docs/core/game-context/data-service) * [Board Service](/docs/core/game-context/board-service) * [Wallet Service](/docs/core/game-context/wallet-service) * [RNG Service](/docs/core/game-context/rng-service) ### state The core game state exposes some underlying state and information about the current simulation. [Further reading](/docs/core/game-context/state) # RNG Service (/docs/core/game-context/rng-service) *** ## Introduction The RNG service is connected to the underlying program. If you ever need a random outcome in your game implementation, use this seeded RNG service for reproducible results. ## Methods ### weightedRandom() | Method | Type | | ----------------------------------- | --------------------- | | `ctx.services.rng.weightedRandom()` | `(weights) => string` | #### Parameters \[!toc] | Parameter | Type | | --------- | ------------------------ | | `weights` | `Record` | Weighted draw of key from a mapping of keys to weights. ### randomItem() | Method | Type | | ------------------------------- | -------------- | | `ctx.services.rng.randomItem()` | `(array) => T` | #### Parameters \[!toc] | Parameter | Type | | --------- | ---------- | | `array` | `Array` | Gets a random item from an array. ### shuffle() | Method | Type | | ---------------------------- | ------------------ | | `ctx.services.rng.shuffle()` | `(array) => Array` | #### Parameters \[!toc] | Parameter | Type | | --------- | ------- | | `array` | `Array` | Shuffles an array. ### randomFloat() | Method | Type | | -------------------------------- | ----------------------- | | `ctx.services.rng.randomFloat()` | `(low, high) => number` | #### Parameters \[!toc] | Parameter | Type | | --------- | -------- | | `low` | `number` | | `high` | `number` | Gets a random float between two numbers. # State (/docs/core/game-context/state) *** ## Introduction The core game state holds useful information about the current simulation. Most of the state is handled automatically during simulation, or when calling service methods. You won't need to touch most of it. Defined [custom additional state](/docs/core/config/custom-state) is available under `ctx.state.userData` Wrong usage can break simulations! ## Properties | Property | Type | Description | | ----------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `currentGameMode` | `string` | The name of the currently simulated game mode.
**Serves internal purposes only. Do not modify!** | | `currentSpinType` | `string` | As defined under `SPIN_TYPE`. | | `currentResultSet` | `ResultSet` | The current result set. Useful to conditionally do things to achieve certain outcomes.
**Serves internal purposes only. Do not modify!** | | `isCriteriaMet` | `boolean` | **Serves internal purposes only. Do not modify!** | | `currentFreespinAmount` | `number` | Stores the number of remaining free spins. Increased using `awardFreespins()` from game service. Reduce manually. | | `totalFreespinAmount` | `number` | Stores the number of total awarded free spins. Increased using `awardFreespins()` from game service. Don't reduce. | | `triggeredFreespins` | `boolean` | Whether free spins were triggered. Set using `awardFreespins()` from game service. | | `triggeredMaxWin` | `boolean` | Whether a max win was triggered. Set automatically at the end of a simulation. | | `userData` | `Record` | Custom user state as defined in the game config. Do with it what you want. | # Wallet Service (/docs/core/game-context/wallet-service) *** ## Introduction The underlying wallet state holds information on **several types of win data**. You should **understand the intentions and use cases** of the wallet state to ensure correct usage of this service. Each win saving state holds a number representing the bet multiplier payout. ### Win \[!toc] Internally called `currentWin`, this state saves the total wins of a single simulation. This value must be updated by the user by calling `confirmSpinWin()`, after wins have been added with `addSpinWin()`. **This value is read by the program to determine the final payout of a simulation.** ### Spin Win \[!toc] The spin win (`currentSpinWin`) holds information about the win of a single spin. During free spins logic, the spin win must be confirmed/reset before each new free spin. ### Tumble Win \[!toc] A tumble win (`currentTumbleWin`) is accumulated by calling `addTumbleWin()`. Effectively, this state isn't too different from the spin win. It serves the purpose of having a separate state to store tumble wins in - specifically for event recording / frontend display purposes. ## Methods ### addSpinWin() | Method | Type | | ---------------------------------- | ------------------ | | `ctx.services.wallet.addSpinWin()` | `(amount) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | -------- | | `amount` | `number` | This method adds the given amount to the wallet state. After calculating the win for a board, call this method to update the wallet state. If your game has tumbling mechanics, you should call this method again after every new tumble and win calculation. ### addTumbleWin() | Method | Type | | ------------------------------------ | ------------------ | | `ctx.services.wallet.addTumbleWin()` | `(amount) => void` | #### Parameters \[!toc] | Parameter | Type | | --------- | -------- | | `amount` | `number` | Helps to add tumble wins to the wallet state. This also calls `addSpinWin()` internally, to add the tumble win to the overall spin win. ### confirmSpinWin() | Method | Type | | -------------------------------------- | ------------ | | `ctx.services.wallet.confirmSpinWin()` | `() => void` | Confirms the wins of the current spin. Should be called after `addSpinWin()`, and after your tumble events are played out, and after a (free) spin is played out to finalize the win. ### getCurrentWin() | Method | Type | | ------------------------------------- | -------------- | | `ctx.services.wallet.getCurrentWin()` | `() => number` | Gets the current total win of the simulation. ### getCurrentSpinWin() | Method | Type | | ----------------------------------------- | -------------- | | `ctx.services.wallet.getCurrentSpinWin()` | `() => number` | Gets the total win of the current spin or simulation. ### getCurrentTumbleWin() | Method | Type | | ------------------------------------------- | -------------- | | `ctx.services.wallet.getCurrentTumbleWin()` | `() => number` | Gets the current total tumble win. # Implementing your Game (/docs/core/game-implementation) *** ## Quick Start The entire game flow will live inside a single hook that is passed to the game configuration. ```ts lineNumbers export const game = createSlotGame({ /* the rest of your configuration */ hooks: { onHandleGameFlow(ctx) { // implement your game here }, }, }) ``` As a game implementation might be a couple hundred lines long, you could **write your implementation in a different file**. ```ts lineNumbers import { GameContext } from "@slot-engine/core" import { GameModesType, SymbolsType, UserStateType } from "./your-main-file" type Context = GameContext export function onHandleGameFlow(ctx: Context) { // ... } ``` Check out a complete game flow example [on our GitHub](https://github.com/slot-engine/slot-engine/blob/main/examples/cluster_example/src/onHandleGameFlow.ts). ## Dos and Don'ts ### ✅ Do \[!toc] #### Pass game context around \[!toc] If you want to separate your game implementation into multiple functions to avoid code duplication, you can safely pass the context between functions. ```ts lineNumbers export function onHandleGameFlow(ctx: Context) { drawBoard(ctx) } function drawBoard(ctx: Context) { const reels = ctx.services.board.getRandomReelset() // ... } ``` ### ❌ Don't \[!toc] #### Destroy reference to the context object \[!toc] Your simulations will not work as intended if you do this. ```ts lineNumbers export function onHandleGameFlow(ctx: Context) { const nope = { ...ctx } const yesButWhy = ctx } ``` ## Game Flow Visualization To help you understand slot game flows, here is a graphic showing a general game flow. Game Flow Graphic ## Next Steps Learn about the [context object](/docs/core/game-context) and what role it plays when implementing your game. # Win Calculation (/docs/core/game-implementation/win-calculation) *** ## Introduction There are multiple approaches for calculating wins, depending of the type of game you're going for. ### Context \[!toc] Each win type operates disconnected from the game context, that's why you have to explicitly pass the game context to the win type class constructor. ### Configuration \[!toc] The configuration options depend on the win type. ### Wild Symbol \[!toc] To ensure wild symbols are recognized and handled correctly during calculation, you must pass one of the following to the win type constructor: A complete game symbol ... ```ts lineNumbers const wildSymbol = ctx.config.symbols.get("W") const lines = new LinesWinType({ wildSymbol, // ... }) ``` ... or a property that identifies one or more symbols. ```ts lineNumbers const lines = new LinesWinType({ wildSymbol: { isWild: true }, // ... }) ``` ### Post-Processing \[!toc] After calculating wins you have the option to process them further, e.g. multiply by a global multiplier. ```ts lineNumbers const { payout, winCombinations } = lines .evaluateWins(ctx.services.board.getBoardReels()) .postProcess((wins, ctx) => { // Example: Apply 2x multiplier during free spins let multiplier = 1 if (ctx.state.currentSpinType === SPIN_TYPE.FREE_SPINS) { multiplier = 2 } return { winCombinations: wins.map((w) => ({ ...w, payout: w.payout * multiplier, })), } }) .getWins() ``` `postProcess` should return the modified win combinations. The resulting total `payout` from `getWins()` will be recalculated based on your modifications to the win combinations. While you're not forced to use `postProcess()` to modify the win data, it provides an opinionated way of writing cohesive win evaluation logic. ## Paylines Wins Calculation of classic line-based wins. ```ts lineNumbers import { LinesWinType } from "@slot-engine/core" export function onHandleGameFlow(ctx: Context) { const wildSymbol = ctx.config.symbols.get("W") const lines = new LinesWinType({ ctx, lines: { 1: [0, 0, 0, 0, 0], 2: [1, 1, 1, 1, 1], 3: [2, 2, 2, 2, 2], // ... }, wildSymbol, }) const { payout, winCombinations } = lines .evaluateWins(ctx.services.board.getBoardReels()) .getWins() } ``` ## Cluster Wins Cluster-based win calculation. ```ts lineNumbers import { ClusterWinType } from "@slot-engine/core" export function onHandleGameFlow(ctx: Context) { const wildSymbol = ctx.config.symbols.get("W") const cluster = new ClusterWinType({ ctx, wildSymbol, }) const { payout, winCombinations } = cluster .evaluateWins(ctx.services.board.getBoardReels()) .getWins() } ``` ## Manyways Wins Megaways or 243-ways -esque calculations. ```ts lineNumbers import { ManywaysWinType } from "@slot-engine/core" export function onHandleGameFlow(ctx: Context) { const wildSymbol = ctx.config.symbols.get("W") const ways = new ManywaysWinType({ ctx, wildSymbol, }) const { payout, winCombinations } = ways .evaluateWins(ctx.services.board.getBoardReels()) .getWins() } ``` # Analyzing your Game (/docs/core/game-tasks/analysis) *** ## Introduction Configure analysis to get useful statistics about your game. Analyzing your game is quite simple: Simply adjust your `runTasks()` configuration to include analysis: ```ts lineNumbers=11 game.runTasks({ // ... doAnalysis: true, analysisOpts: { gameModes: ["base"], }, }) ``` ## Output Files / Publish Files The output will be written to the `__build__` directory. ### stats\_payouts.json \[!toc] Contains a list of win ranges and how many simulations (from the final lookup table) fall into those ranges. ### stats\_summary.json \[!toc] Contains various statistics about volatility, hit rates, etc. # Optimizing your Game (/docs/core/game-tasks/optimization) *** ## Introduction After simulating and analyzing the outcomes of your game, you may notice that your RTP may be too high or too low and that the game may generally be unbalanced. To achieve a desired target RTP between 94% and 98%, the optimization program can be used. **A big thanks to Stake** for providing the entire game optimizer code. [(Original source code)](https://github.com/StakeEngine/math-sdk/tree/main/optimization_program) The optimization program (from 13.08.25) has been integrated into Slot Engine, enabling you to precisely adjust your games to achieve the desired RTP. Additionally, you can further tweak the weights of certain payout groups using custom configs. After optimization is done, you can observe that the weights in your lookup tables have been redistributed. Before: ```csv 1,1,780 2,1,1000 3,1,0 ... ``` After: ```csv 1,1816455674,780 2,58062661,1000 3,19165815565,0 ... ``` The optimizer works, but **it would be nice to not depend on Stake** updating their original optimizer code. Since their optimizer is designed to work with their math SDK, integrating new optimizer changes into Slot Engine might not always work. Also their optimizer is not well documented. All info on this page is gathered from the **Stake Engine Discord**. **Go there if you have specific questions about the optimizer not documented here.** I don't know Rust and have no mathematical background, and the optimization algorithm seems complex. If anyone would like to try to **implement such an algorithm in TypeScript**, feel free to open an issue and let's talk about it. Having a **first-party** optimization algorithm would be **good in the long run**. ## Configuration Install [Rust and cargo](https://rust-lang.org/tools/install/) if you haven't already. This is necessary because the optimization program is written in Rust. Ensure your simulations cover a wide range of outcomes. Otherwise the optimizer might struggle to work properly. You can configure [analysis](/docs/core/game-tasks/analysis) to get an overview of all payout ranges and how often certain outcomes appear. Ensure there are sufficient outcomes for each payout range. Call `configureOptimization()` on your game ```ts lineNumbers const game = createSlotGame({ /* ... */ }) game.configureOptimization({ gameModes: { base: { conditions: { maxwin: new OptimizationConditions({ rtp: 0.01, avgWin: 2000, searchConditions: { criteria: "maxwin", }, priority: 8, }), "0": new OptimizationConditions({ rtp: 0, avgWin: 0, searchConditions: 0, priority: 6, }), superFreespins: new OptimizationConditions({ rtp: 0.05, hitRate: 500, searchConditions: { criteria: "superFreespins", }, priority: 3, }), freespins: new OptimizationConditions({ rtp: 0.35, hitRate: 150, searchConditions: { criteria: "freespins", }, priority: 2, }), basegame: new OptimizationConditions({ rtp: 0.55, hitRate: 4, priority: 1, }), }, scaling: new OptimizationScaling([]), parameters: new OptimizationParameters(), }, // ... }, }) ``` Finally call `runTasks()` on your game ```ts lineNumbers=11 game.runTasks({ doSimulation: true, doOptimization: true, optimizationOpts: { gameModes: ["base"], }, }) ``` * `configureOptimization` has a `gameModes` property specifying optimization configurations for different game modes. * The keys of `gameModes` must match the modes in your game configuration. * Each game mode has 3 configuration objects: `conditions`, `scaling` and `parameters`. ### Optimization Conditions Each `conditions` key for a game mode can have multiple sub-properties (conditions). A condition is used to **select specific simulations which the optimization is applied to**. Each `ResultSet` of a game mode must have a corresponding optimization condition, named after its criteria. Since you probably want to optimize every single simulation, you should define as many conditions for a game mode as you need, to enable optimization for all simulations. For an optimization condition, specify the property `searchConditions` and set key/value pairs which the optimizer will search for in the [force records file](/docs/core/game-context/data-service#recorder). The optimizer requires knowing what RTP to optimize a subset of outcomes to / how much RTP to contribute to the total RTP. This is especially useful for defining the hit frequency of free spins, for example. **2 of 3 variables need to be defined**, so the program can determine the target RTP for a set of results. These being: `rtp`, `avgWin`, and/or `hitRate`. Each condition is given a `priority`. This determines the **order** in which simulations are picked out of the pool of all simulations. Setting priorities correctly is **very important**, because once a simulation is optimized, it can't be optimized again. Let's break it down with an example: 1. You have two simulations: ID 1 - free spins, and ID 2 - free spins max win. 2. Both simulations are tagged in the records as "triggeredFreeSpins". 3. You have two optimizer conditions: Priority 5 - selects free spins, and priority 1 - selects max wins. 4. The free spins subset with 5 priority should get a target hit rate of 1/200, and the max win subset with 1 priority should get a target hit rate of 1/500.000. 5. The optimizer first works on simulations that match the conditions with the highest priority. In this case it would get all simulations tagged as "triggeredFreeSpins" and optimize them. 6. The "normal free spins" will get optimized and receive their target hit rate. But since the max win simulations are also tagged as being free spins, those will get optimized too and result in a hit rate which is way too high. 7. All optimized simulations are removed from the pool. 8. When it's time to optimize the max wins (with priority 1), there are actually no simulations left in the pool that match the criteria, since all max wins have already been (wrongly) optimized. This will not only result in an error, but also your game will be unbalanced. **The highest priority should always be given to the most specific simulation subsets.** Fortunately, **Slot Engine automatically tags** every simulation **by their result set criteria**, creating groups of simulations that **don't overlap** one another. When creating your optimization conditions, you can **simply specify the result set criteria** as the only key in `searchConditions`. This way the priority won't matter at all (given you did not add additional records which may cause overlap). ```ts lineNumbers freespinsUpgradeToSuper: new OptimizationConditions({ rtp: 0.03, hitRate: 500, searchConditions: { criteria: "freespinsUpgradeToSuper", }, priority: 4, }), ``` Since the optimizer is created by Stake and not the Slot Engine team, [consult their docs](https://stake-engine.com/docs/math/optimization-algorithm) for any more information you may need, or ask in the Stake Engine Discord server. #### Properties | Property | Type | Description | Required | | ------------------ | ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `rtp` | `number \| "x"` | The desired RTP contribution.
Total RTP of all optimization conditions should equal RTP in your game configuration. | | | `avgWin` | `number` | The desired average win per spin. | | | `hitRate` | `number \| "x"` | The desired hit rate (e.g. `200` to hit 1 in 200 spins). | | | `searchConditions` | `number \| Record \| [number, number]` | - A number (payout multiplier), e.g. `5000`
- Force record values, e.g. `{ "symbolId": "scatter" }`
- Payout multiplier range, e.g. `[0, 100]` | | | `priority` | `number` | As explained in the paragraphs above. | yes | At most one optimization condition of a game mode can have either `rtp: "x"` or `hitRate: "x"`. The optimizer can determine one missing value automatically. ### Optimization Scaling With scaling you can artifically increase the chances of a certain win range being hit. **This should be used carefully!** A `new OptimizationScaling([])` **must** be provided, even if you don't intend to utilize it. You can just pass an empty array to the constructor. ```ts lineNumbers scaling: new OptimizationScaling([ { criteria: "freespins", scaleFactor: 1.2, winRange: [50, 150], probability: 1, }, // ... ]) ``` #### Properties All properties are required. | Property | Type | Description | | ------------- | ------------------ | --------------------------------------------------------------------------- | | `criteria` | `string` | The criteria name of a result set or optimization conditions, respectively. | | `scaleFactor` | `number` | The factor which the odds of hitting the win range is multiplied by. | | `winRange` | `[number, number]` | The target win range, e.g. `[50, 150]`. | | `probability` | `number` | The probability (0-1) of the scaling being actually applied. Typically 1. | Since the optimizer is created by Stake and not the Slot Engine team, [consult their docs](https://stake-engine.com/docs/math/optimization-algorithm) for any more information you may need, or ask in the Stake Engine Discord server. ### Optimization Parameters Configure special parameters to control the optimization algorithm. You can just pass `new OptimizationParameters()` to apply the default settings. #### Properties All properties are optional. | Property | Type | | -------------------------- | ---------- | | `numShowPigs` | `number` | | `numPigsPerFence` | `number` | | `threadsFenceConstruction` | `number` | | `threadsShowConstruction` | `number` | | `testSpins` | `number[]` | | `testSpinsWeights` | `number[]` | | `simulationTrials` | `number` | | `graphIndexes` | `number[]` | | `run1000Batch` | `false` | | `minMeanToMedian` | `number` | | `maxMeanToMedian` | `number` | | `pmbRtp` | `number` | | `scoreType` | `"rtp"` | Since the optimizer is created by Stake and not the Slot Engine team, [consult their docs](https://stake-engine.com/docs/math/optimization-algorithm) for any more information you may need, or ask in the Stake Engine Discord server. # Simulating your Game (/docs/core/game-tasks/simulation) *** ## Introduction Configuring a game alone isn't enough - it also has to be simulated to ensure correct functionality. Simulating your game is quite simple: Call `configureSimulation()` on your game ```ts lineNumbers const game = createSlotGame({ /* ... */ }) game.configureSimulation({ simRunsAmount: { base: 100_000, bonus: 100_000, }, concurrency: 16, }) ``` * `simRunsAmount` defines the amount of simulations for each game mode. For quick and dirty testing you can do 10\_000 simulations, but for production a minimum of 500\_000 is recommended. If you do not wish to simulate certain game modes, just exclude them from `simRunsAmount`. * `concurrency` controls the amount of threads (Node workers) used for simulation. [See all options](#options) Finally call `runTasks()` on your game ```ts lineNumbers=11 game.runTasks({ doSimulation: true, }) ``` Simulation time can range from a few seconds to several minutes, depending on game modes, simulation count, thread concurrency, and your hardware. **Ensure sufficient disk space** for large and complex games with many events. Slot Engine offloads in-memory data to temporary files which grow in size as your game is simulating. **Large games with 5+ million simulations can temporarily take up 10-20 GB of disk space.** ### configureSimulation() options | Property | Type | Description | Required | | ---------------- | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | | `simRunsAmount` | `Record` | List of game modes and their simulation count. | yes | | `concurrency` | `number` | Number of threads (Node worker threads) to use. More threads = faster simulation, but higher CPU usage.
Default: 6 | | | `maxPendingSims` | `number` | The maximum number of simulation results to keep pending in memory before writing to disk. Higher values may speed up simulations but use more RAM.
Default: 250 | | | `maxDiskBuffer` | `number` | The maximum data buffer in MB for writing simulation results to disk. Higher values may speed up simulations but use more RAM.
Default: 50 | | ## Output Files / Publish Files The output will be written to the `__build__/publish_files` directory. Those are the **final files** that can be uploaded to Stake Engine as the "math" part. ### books\_\.jsonl.zst \[!toc] For each game mode a books file is generated which contains a list of all books (simulations). That file is compressed using Zstandard compression, because book files tend to get quite large the more events you have. ```jsonl {"id":1,"payoutMultiplier":780,"events":[{"index":1,"type":"test","data":{"test":123}}]} {"id":2,"payoutMultiplier":1000,"events":[{"index":1,"type":"test","data":{"test":123}}]} {"id":3,"payoutMultiplier":0,"events":[{"index":1,"type":"test","data":{"test":123}}]} ... ``` ### index.json \[!toc] An overview of all simulated game modes and their corresponding file names. ### lookUpTable\_\\_0.csv \[!toc] The lookup table contains a list of all book IDs, a weight, and the payout multiplier (scaled by 100). A book's weight determines the probability of that outcome being selected by the Stake RGS when resolving a bet. **By default**, all results have an **equal probability** of being selected. ```csv ID, weight, payout 1,1,780 2,1,1000 3,1,0 ... ``` Since every outcome has the same weight, initial game RTP will likely be too high or too low - this is completely normal. You will use [optimization](/docs/core/game-tasks/optimization) to automatically redistribute weights to achieve a specific target RTP with high precision. ## FAQ / Common Issues The most common reason for why your simulation might be stuck or progress very slowly is that certain **outcomes are very rare** or might **never occur** naturally, for example max wins. If you're noticing that simulating max wins or other rare criteria slows down your simulation, there's a few things you can do: * Make it easier for the program to simulate rare outcomes, for example by using a dedicated reel set with more wilds and generally higher symbols. * Programmatically force the desired outcome (board setup, multipliers, etc.) If Node runs out of memory, you have a few options to consider: * Reduce the amount of simulations for a game mode. You generally don't need more than 1-2 million simulations per game mode * Increase memory limit in Node * Tune the simulation options, specifically `maxPendingSims` and/or `maxDiskBuffer`