# App & Asset Loading Source: https://www.mintlify.com/StakeEngine/web-sdk/api/app The App component and asset loading system for PixiJS applications ## Overview The `` component is the root of every Stake Engine game. It initialises a `PIXI.Application`, mounts the canvas, loads all declared assets, and exposes reactive state through the `stateApp` object created by `createApp`. The component tree assembled by `` is: ``` └─ — creates PIXI.Application, appends canvas └─ — sets up the PixiJS display-object parent context └─ — loads all assets declared in stateApp.assets └─ {children} ``` *** ## createApp Create an application state object and pass it to your Svelte context before mounting ``. ```typescript theme={null} import { createApp } from 'pixi-svelte'; import type { Assets } from 'pixi-svelte'; const assets: Assets = { logo: { type: 'sprite', src: '/images/logo.png' }, character: { type: 'spine', src: { skeleton: '/spine/char.skel', atlas: '/spine/char.atlas' }, }, coins: { type: 'spriteSheet', src: '/spritesheets/coins.json' }, ui: { type: 'sprite', src: '/images/ui.png', preload: true }, }; const { stateApp } = createApp({ assets }); ``` ### stateApp fields The asset manifest passed to `createApp`. A dictionary mapping string keys to `Asset` descriptors. Each descriptor has a `type` (`'sprite' | 'sprites' | 'spriteSheet' | 'spine' | 'font' | 'audio'`), a `src`, and an optional `preload` flag. Becomes `true` once all non-preload assets have finished loading. Reactive — read it inside a `$derived` or `$effect` to react to load completion. A number from `0` to `100` representing the percentage of non-preload assets loaded. Useful for rendering a progress bar. A dictionary of fully processed assets keyed by the same names used in the manifest. Components such as ``, ``, and `` look up assets from this object using their `key` prop. The underlying `PIXI.Application` instance. Available after `` has mounted. `undefined` before initialisation and after destroy. Resets `loaded`, `loadingProgress`, `loadedAssets`, and `pixiApplication` to their initial values. Called automatically by `` on mount and destroy. *** ## \ The top-level wrapper for a Stake Engine game. Place it inside a Svelte component that has called `setContext` with the result of `createApp`. ```svelte theme={null} ``` ### Props The Svelte snippet rendered inside the fully-initialised application. Children are only rendered after `` has created the canvas **and** all preload assets are available. `` calls `stateApp.reset()` on both mount and destroy. This ensures the application starts from a clean state and tears down cleanly when the component is removed. *** ## \ Internal component used by ``. Reads the asset manifest from `stateApp.assets`, loads any `preload: true` assets first (blocking render), then loads the remaining assets in the background while updating `stateApp.loadingProgress`. You do not need to use `` directly — `` includes it automatically. ### How asset loading works **Preload assets** (`preload: true`) are loaded before children are rendered. Use this for assets required to display the initial frame — for example, a loading screen background. **Deferred assets** (default, `preload: false`) are loaded after children are rendered. `stateApp.loadingProgress` increments as each one finishes. `stateApp.loaded` becomes `true` when all deferred assets are done. *** ## \ Internal component used by ``. Creates the `PIXI.Application` and appends its canvas to the DOM. The application is configured with: | Option | Value | | ----------------- | -------------------------------- | | `autoDensity` | `true` | | `backgroundAlpha` | `0` (transparent) | | `antialias` | `true` | | `preference` | `'webgpu'` (falls back to WebGL) | | `powerPreference` | `'high-performance'` | | `resolution` | `devicePixelRatio` (reactive) | | `resizeTo` | `window` | Touch-action is set to `'auto'` on the canvas to prevent blocking page scroll on touch devices. Children are rendered only after the application has finished initialising. *** ## Asset manifest types ```typescript theme={null} import type { Assets, Asset, SpineSrc } from 'pixi-svelte'; // A single asset entry type Asset = { type: 'sprite' | 'sprites' | 'spriteSheet' | 'spine' | 'font' | 'audio'; src: string | SpineSrc; preload?: boolean; // defaults to false }; // Spine assets require skeleton + atlas paths type SpineSrc = { skeleton: string; atlas: string; scale?: number; }; // Full manifest type Assets = Record; ``` *** ## Usage example ```svelte theme={null} {#if !stateApp.loaded} {:else} {/if} ``` # Container & Graphics Source: https://www.mintlify.com/StakeEngine/web-sdk/api/container-and-graphics Layout containers and procedural drawing components ## Overview Containers group display objects and control their position, visibility, and layer order. Graphics components let you draw shapes programmatically using the PixiJS graphics API. ### Coordinate system PixiJS uses a **top-left origin** — `x=0, y=0` is the top-left corner of the canvas. The `x` axis increases to the right, and the `y` axis increases downward. The `anchor` prop on shape components shifts the pivot point of the object. `anchor={0}` (default) places the origin at the top-left of the object. `anchor={0.5}` centres it. `anchor={1}` places it at the bottom-right. *** ## \ A transparent display-object group. Nest other components inside it to control their collective position, scale, alpha, and z-order. `` accepts all props from `PIXI.ContainerOptions`. The most commonly used props are listed below. ### Props Horizontal position in pixels from the parent's origin. Default `0`. Vertical position in pixels from the parent's origin. Default `0`. Override the container's width. Scales child content to fit. Override the container's height. Scales child content to fit. Uniform scale (`number`) or per-axis scale (`{ x, y }`). Default `1`. Opacity from `0` (invisible) to `1` (fully opaque). Default `1`. When `false`, the container and all its children are hidden and excluded from rendering. Default `true`. Sort order within the parent container. Higher values render on top. Default `0`. Rotation in radians. Default `0`. The pivot point used for rotation and scaling, in local coordinates. When `true`, children are automatically sorted by `zIndex` before each render. Default `false`. When `false`, pointer events are disabled for all children. Default `true`. CSS cursor string shown when the pointer is over this container. Accepts standard CSS cursor values. Child components to render inside this container. ### Example ```svelte theme={null} ``` *** ## \ Renders arbitrary shapes using a draw callback. The callback receives a `PIXI.Graphics` instance and is re-run reactively whenever the `draw` prop changes. ### Props A function called with the `PIXI.Graphics` object. Use the full PixiJS graphics API inside this function to draw shapes, lines, fills, and strokes. The graphics context is cleared before each call. When `true`, this graphics object is applied as a mask to the parent container. When `false` or omitted, the mask is removed. Default `undefined`. Horizontal position. Default `0`. Vertical position. Default `0`. Opacity from `0` to `1`. Default `1`. Whether the graphic is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string for pointer interaction. ### Example ```svelte theme={null} { g.roundRect(0, 0, 400, 20, 10); g.fill({ color: 0x333333 }); g.roundRect(0, 0, 400 * progress, 20, 10); g.fill({ color: 0x00cc66 }); }} /> ``` *** ## \ A convenience wrapper around `` that draws a filled circle with an optional border. Supports the `anchor` prop to control the pivot point. ### Props The diameter of the circle in pixels. Controls the pivot point. `0` = top-left, `0.5` = centre, `1` = bottom-right. Accepts a number (uniform) or `{ x, y }`. Default `undefined` (top-left). Fill colour. Accepts hex numbers (`0xff0000`), CSS colour strings (`'#ff0000'`), or RGB arrays. Default `0x000000`. Fill opacity from `0` to `1`. Default `1`. Stroke colour. Default `0x000000`. Stroke width in pixels. Default `0` (no border). Stroke opacity from `0` to `1`. Default `1`. Horizontal position. Default `0`. Vertical position. Default `0`. Object opacity. Default `1`. Whether the circle is rendered. Default `true`. Sort order within the parent. Default `0`. Apply this circle as a mask on the parent container. CSS cursor string. ### Example ```svelte theme={null} ``` *** ## \ A convenience wrapper around `` that draws a filled rectangle with optional rounded corners and border. Supports the `anchor` prop. ### Props Width of the rectangle in pixels. Height of the rectangle in pixels. Controls the pivot point. `0` = top-left, `0.5` = centre. Default `undefined` (top-left). Corner radius for rounded rectangles. `0` = sharp corners. Default `0`. Fill colour. Default `0x000000`. Fill opacity from `0` to `1`. Default `1`. Stroke colour. Default `0x000000`. Stroke width in pixels. Default `0` (no border). Stroke opacity from `0` to `1`. Default `1`. Horizontal position. Default `0`. Vertical position. Default `0`. Object opacity. Default `1`. Whether the rectangle is rendered. Default `true`. Sort order within the parent. Default `0`. Apply this rectangle as a mask on the parent container. CSS cursor string. ### Example ```svelte theme={null} ``` *** ## Using Graphics as a mask Set `isMask={true}` on any ``, ``, or `` to clip the parent container to that shape. ```svelte theme={null} ``` # createEventEmitter Source: https://www.mintlify.com/StakeEngine/web-sdk/api/create-event-emitter Type-safe event emitter factory for connecting game logic to Svelte components `createEventEmitter` creates a typed pub/sub channel. Game logic calls `broadcast` or `broadcastAsync` to fire events; Svelte components subscribe via `subscribeOnMount` and automatically unsubscribe when they are destroyed. ## Function signature ```typescript theme={null} function createEventEmitter(): { eventEmitter: { broadcast: (emitterEvent: TEmitterEvent) => void; broadcastAsync: (emitterEvent: TEmitterEvent) => Promise; subscribeOnMount: (emitterEventHandlerMap: Partial) => void; }; } ``` ### EmitterEventBase Every event type must extend `EmitterEventBase`: ```typescript theme={null} export type EmitterEventBase = { type: string; }; ``` The `type` field is the discriminant used to route events to handlers. ## Parameters `createEventEmitter` takes no parameters. The event type is supplied as a TypeScript generic: ```typescript theme={null} const { eventEmitter } = createEventEmitter(); ``` ## Return value An object with three methods. ### eventEmitter.broadcast ```typescript theme={null} broadcast(emitterEvent: TEmitterEvent): void ``` Fires an event **synchronously** — all subscriber callbacks are called in the order they were registered, without waiting for any return values. Use `broadcast` for fire-and-forget notifications where you do not need to wait for the subscriber to finish, such as showing or hiding UI elements and updating reactive state. ```typescript theme={null} eventEmitter.broadcast({ type: 'soundOnce', name: 'sfx_win' }); eventEmitter.broadcast({ type: 'boardShow' }); ``` ### eventEmitter.broadcastAsync ```typescript theme={null} broadcastAsync(emitterEvent: TEmitterEvent): Promise ``` Fires an event and returns a `Promise.all` over all subscriber return values. Use `broadcastAsync` when you need to **await** the completion of subscriber work before continuing — for example, waiting for an animation to finish, a countdown to complete, or a transition to resolve. ```typescript theme={null} // Wait for the win animation to complete before hiding the UI await eventEmitter.broadcastAsync({ type: 'winUpdate', amount: 500, winLevelData }); // Wait for the screen transition before changing game state await eventEmitter.broadcastAsync({ type: 'transition' }); ``` All subscribers are called with `Promise.all`. If any subscriber returns a rejected promise, `broadcastAsync` will reject. Ensure subscriber handlers handle their own errors where necessary. ### eventEmitter.subscribeOnMount ```typescript theme={null} subscribeOnMount( emitterEventHandlerMap: Partial<{ [T in TEmitterEvent['type']]: (emitterEvent: Extract) => any; }> ): void ``` Registers a map of event handlers inside a Svelte component. Internally calls Svelte's `onMount`, which means: * Subscription is set up when the component mounts. * The unsubscribe function returned by the internal `subscribeHandlerMap` is called automatically by `onMount`'s cleanup when the component is destroyed. * You do **not** need to manage the subscription lifecycle manually. The handler map is partial — you only need to handle the event types relevant to that component. ```typescript theme={null} // Inside a Svelte component {#if visible}

Score: {score}

{/if} ``` ```typescript theme={null} // game.ts — broadcast from game logic import { eventEmitter } from './eventEmitter'; // Fire-and-forget: update score reactively eventEmitter.broadcast({ type: 'scoreUpdate', score: 100 }); // Await animation completion before continuing await eventEmitter.broadcastAsync({ type: 'animateScore', target: 100 }); // Hide the score display eventEmitter.broadcast({ type: 'scoreHide' }); ``` ### Using context ```svelte theme={null} ``` ```svelte theme={null} ``` ### Real-world pattern (from the Lines game) The Lines game creates a single emitter that is shared across all game logic and UI components: ```typescript theme={null} // apps/lines/src/game/eventEmitter.ts import { createEventEmitter } from 'utils-event-emitter'; import type { EmitterEventHotKey } from 'components-shared'; import type { EmitterEventUi } from 'components-ui-pixi'; import type { EmitterEventModal } from 'components-ui-html'; import type { EmitterEventGame } from './typesEmitterEvent'; export type EmitterEvent = | EmitterEventHotKey | EmitterEventUi | EmitterEventModal | EmitterEventGame; export const { eventEmitter } = createEventEmitter(); ``` The union type aggregates event definitions from every package that contributes events. Each package exports its own `EmitterEvent*` type, keeping event definitions co-located with the components that handle them. # createPlayBookUtils Source: https://www.mintlify.com/StakeEngine/web-sdk/api/create-play-book-utils Factory function for processing ordered sequences of bookEvents `createPlayBookUtils` returns a pair of functions — `playBookEvent` and `playBookEvents` — that drive a game book: an ordered list of events returned by the RGS (e.g. `reveal`, `winInfo`, `setWin`). Each event is dispatched to its matching handler and awaited before the next one starts. ## Function signature ```typescript theme={null} function createPlayBookUtils< TBookEventHandlerMap extends BookEventHandlerMap >(options: { bookEventHandlerMap: TBookEventHandlerMap; debug?: boolean; }): { playBookEvent: (bookEvent: TBookEvent, bookEventContext: TBookEventContext) => Promise; playBookEvents: (bookEvents: TBookEvent[], bookEventContext?: BookEventContextFromMapWithoutBookEvents) => Promise; } ``` ## Parameters A map of `bookEvent.type` strings to async handler functions. The generic type parameter `TBookEventHandlerMap` must extend `BookEventHandlerMap`, which gives TypeScript the information it needs to infer `TBookEvent` and the context shape from a single source of truth. Each handler has the signature: ```typescript theme={null} (bookEvent: TBookEvent, context: THandlerContext) => Promise ``` When `true`, logs each `bookEvent` object to the console via `console.log` before its handler is called. Useful during development to trace book playback. ## Return value Plays a single book event. Looks up `bookEvent.type` in `bookEventHandlerMap`, then awaits the matching handler. If no handler is found, logs an error to the console — it does **not** throw. ```typescript theme={null} const playBookEvent = async ( bookEvent: TBookEvent, bookEventContext: TBookEventContext, ) => Promise ``` `TBookEventContext` is the handler context type merged with `{ bookEvents: TBookEvent[] }`, so every handler receives the full book alongside the current event. Plays a list of book events **in sequence** — one at a time, in order — using an internal `sequence()` utility (not `Promise.all`). This is intentional: game events have strict ordering requirements. A reel must finish spinning before wins are shown; free-spin triggers must complete before the next spin begins. ```typescript theme={null} const playBookEvents = async ( bookEvents: TBookEvent[], bookEventContext?: BookEventContextFromMapWithoutBookEvents, ) => Promise ``` If `bookEventContext` is omitted, an empty object is used. The full `bookEvents` array is automatically injected into the context passed to each handler, so handlers can inspect the complete book (e.g. to detect bonus rounds). ## TypeScript types These types are exported from `utils-book`. ```typescript theme={null} // Every book event must carry an index and a discriminant type string. export type BaseBookEvent = { index: number; type: string }; // A handler for a specific book event type and context. export type BookEventHandler< TBookEvent extends BaseBookEvent, THandlerContext extends object, > = (bookEvent: TBookEvent, context: THandlerContext) => Promise; // The map of type strings to handlers. export type BookEventHandlerMap< TBookEvent extends BaseBookEvent, THandlerContext extends object, > = Record>; // Extract the BookEvent union from a BookEventHandlerMap. export type GetBookEventFromMap> = T extends BookEventHandlerMap ? U : never; // Extract the handler context type from a BookEventHandlerMap. export type GetBookEventContextFromMap> = T extends BookEventHandlerMap ? U : never; ``` `GetBookEventFromMap` and `GetBookEventContextFromMap` let you derive types from the map without repeating yourself: ```typescript theme={null} type MyBookEvent = GetBookEventFromMap; type MyBookEventContext = GetBookEventContextFromMap; ``` ## createMultiBookUtils For games that play **multiple books simultaneously** (e.g. side-by-side bonus rounds), `createMultiBookUtils` provides two utilities: * **`splitBookToRounds`** — splits a single flat book array into rounds, breaking on a specified set of event types. * **`booksToRounds`** / **`roundsToBooks`** — zips multiple books into interleaved rounds so that round 1 from each book plays together, then round 2, and so on. Use `createMultiBookUtils` when you need to synchronise multiple books round-by-round rather than playing them serially. For single-book games, `createPlayBookUtils` is sufficient. ```typescript theme={null} const { booksToRounds, roundsToBooks } = createMultiBookUtils(); const rounds = booksToRounds({ books: [bookA, bookB], splitBy: ['reveal'] }); // rounds[0] = [eventsFromBookABeforeFirstReveal, eventsFromBookBBeforeFirstReveal] // rounds[1] = [revealFromBookA, revealFromBookB] // ... const remerged = roundsToBooks({ rounds }); ``` ## Usage example ```typescript theme={null} import { createPlayBookUtils, type BookEventHandlerMap } from 'utils-book'; // 1. Define the BookEvent union type type BookEvent = | { index: number; type: 'reveal'; board: string[][] } | { index: number; type: 'setTotalWin'; amount: number } | { index: number; type: 'finalWin'; amount: number }; type BookEventContext = { bookEvents: BookEvent[] }; type BookEventOfType = Extract; // 2. Create the handler map export const bookEventHandlerMap: BookEventHandlerMap = { reveal: async (bookEvent: BookEventOfType<'reveal'>, context: BookEventContext) => { await reelSpin({ board: bookEvent.board }); }, setTotalWin: async (bookEvent: BookEventOfType<'setTotalWin'>) => { totalWin = bookEvent.amount; }, finalWin: async (_bookEvent: BookEventOfType<'finalWin'>) => { // intentionally empty }, }; // 3. Create the utils const { playBookEvent, playBookEvents } = createPlayBookUtils({ bookEventHandlerMap, debug: true, // log each event during development }); // 4. Play a full book returned by the RGS const bet = await requestBet({ sessionID, currency, amount, mode, rgsUrl }); const bookEvents = bet.round.state as BookEvent[]; await playBookEvents(bookEvents); ``` `playBookEvents` uses `sequence()` internally — a sequential async iterator. This is **not** `Promise.all`. Each handler must fully resolve before the next event begins, preserving the visual order of the game. # Particles Source: https://www.mintlify.com/StakeEngine/web-sdk/api/particles Particle system components for visual effects ## Overview The SDK provides two approaches to particles: * **``** — high-level emitter backed by [`@barvynkoa/particle-emitter`](https://github.com/barvynkoa/particle-emitter). Driven by a JSON config and a spritesheet asset key. * **`` + ``** — lower-level approach using PixiJS's native `PIXI.ParticleContainer`. You control initialisation and per-frame update logic directly. *** ## \ A declarative particle emitter. Reads its texture from `stateApp.loadedAssets` using `key`, upgrades the config to the latest format, and drives the emitter on the PixiJS ticker. The emitter is destroyed automatically when the component is unmounted. ### Props Asset manifest key for the particle spritesheet (`type: 'spriteSheet'`). The loaded texture array is passed to the emitter config automatically. The particle emitter configuration object. All three config versions from `@barvynkoa/particle-emitter` are accepted — the SDK upgrades older formats automatically via `upgradeConfig`. When `true`, reinitialises the emitter with the current config. Toggle this to trigger a burst or restart emission. Synced from `Emitter.emit`. A multiplier applied to `ticker.deltaMS` before calling `emitter.update()`. Controls the effective simulation speed. Default `0.00234`. Probability (0–1) that a particle will spawn on any given update. Inherited from `Emitter`. Default `1`. Maximum number of live particles at any time. Inherited from `Emitter`. Custom particle class constructor. Inherited from `Emitter`. ### Emitter config The `config` prop accepts the `EmitterConfigV3` format from `@barvynkoa/particle-emitter`. Key fields: ```typescript theme={null} import type { EmitterConfigV3 } from '@barvynkoa/particle-emitter'; const config: EmitterConfigV3 = { lifetime: { min: 0.5, max: 1.5 }, frequency: 0.008, spawnChance: 1, maxParticles: 100, pos: { x: 0, y: 0 }, addAtBack: false, behaviors: [ { type: 'alpha', config: { alpha: { list: [{ value: 1, time: 0 }, { value: 0, time: 1 }] } }, }, { type: 'scale', config: { scale: { list: [{ value: 1, time: 0 }, { value: 0.3, time: 1 }] } }, }, { type: 'moveSpeed', config: { speed: { list: [{ value: 200, time: 0 }, { value: 100, time: 1 }] } }, }, { type: 'spawnShape', config: { type: 'torus', data: { x: 0, y: 0, radius: 10 } }, }, ], }; ``` ### Example ```svelte theme={null} ``` *** ## \ A wrapper around `PIXI.ParticleContainer` that provides the particle parent context to child `` components. More performant than a regular `` for large numbers of identical sprites. ### Props One or more `` components. An object specifying which properties (`vertices`, `position`, `rotation`, `uvs`, `tint`) are updated per particle. Enabling more properties increases GPU upload cost. From `PIXI.ParticleContainerOptions`. Maximum number of particles. Must be set at creation time. From `PIXI.ParticleContainerOptions`. Number of particles per draw call batch. From `PIXI.ParticleContainerOptions`. Whether the container automatically resizes to accommodate more particles than `maxSize`. From `PIXI.ParticleContainerOptions`. Horizontal position. Default `0`. Vertical position. Default `0`. Opacity. Default `1`. Whether the container is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string. *** ## \ A low-level particle layer. Creates `size` `PIXI.Particle` instances using a texture resolved from `stateApp.loadedAssets`, calls `init` once on mount, and calls `update` on every PixiJS ticker tick. Must be a child of ``. ### Props Asset manifest key for a `sprite` asset. The loaded texture is applied to every particle instance. The number of `PIXI.Particle` instances to create. Called once after the particles are created. Set initial positions, scales, alphas, velocities, etc. Called every frame via the PixiJS ticker. Mutate each particle's properties to simulate motion, fade, etc. The `ParticleContainer` is updated automatically after this callback. ### Example ```svelte theme={null} ``` `` adds particles to the `ParticleContainer` at mount time and never removes them. To hide unused particles, set `alpha = 0` in your `update` function rather than removing them. # RGS Fetcher Source: https://www.mintlify.com/StakeEngine/web-sdk/api/rgs-fetcher Type-safe HTTP client for communicating with the Remote Game Server The **Remote Game Server (RGS)** is the backend that manages game sessions, processes bets, and returns game books (the `state` array of ordered `bookEvent` objects). `rgsFetcher` is a thin, type-safe HTTP client that wraps all RGS communication. Response types are inferred automatically from an auto-generated OpenAPI schema, so endpoint calls are fully typed end-to-end. ## rgsFetcher.post ```typescript theme={null} rgsFetcher.post( options: { url: T; rgsUrl: string; variables?: paths[T]['post']['requestBody']['content']['application/json']; } ): Promise ``` Sends a `POST` request to `https://{rgsUrl}{url}`. The response type `TResponse` defaults to `paths[T]['post']['responses'][200]['content']['application/json']`, so you get full type inference from the schema with no manual annotation required. Logs `console.error` if the HTTP status is not `200`. The RGS endpoint path. Must be one of the paths defined in `schema.ts`. TypeScript will reject any string that is not a valid key. Available POST paths: `'/wallet/authenticate'`, `'/wallet/balance'`, `'/wallet/play'`, `'/wallet/end-round'`, `'/bet/event'`, `'/bet/action'`, `'/session/start'`, `'/game/search'`. The RGS hostname (without protocol), for example `'rgs.example.com'`. The full request URL is constructed as `https://{rgsUrl}{url}`. The request body. The type is inferred from the schema for the given `url`, so TypeScript will tell you exactly which fields are required for each endpoint. ## rgsFetcher.get ```typescript theme={null} rgsFetcher.get( options: { url: T; rgsUrl: string; } ): Promise ``` Sends a `GET` request to `https://{rgsUrl}{url}`. Response type is inferred from the schema the same way as `post`. Logs `console.error` if the HTTP status is not `200`. The RGS endpoint path. The RGS hostname (without protocol). ## Schema and type inference `schema.ts` is auto-generated by [openapi-typescript](https://github.com/drwpow/openapi-typescript) and should not be edited by hand. It exports a `paths` interface that maps every endpoint to its request and response shapes. ```typescript theme={null} // Auto-inferred — no type annotation needed const data = await rgsFetcher.post({ url: '/wallet/authenticate', rgsUrl: 'rgs.example.com', variables: { sessionID: 'abc-123', language: 'en' }, }); // data is typed as components['schemas']['res_authenticate'] ``` ## RGS endpoints The following endpoints are defined in `schema.ts`. ### POST /wallet/authenticate Validates a `sessionID` with the operator. Must be called at game start before any other wallet action. **Request body** (`req_authenticate`): The session ID passed to the game iframe via URL parameters. RGS game language. **Response** (`res_authenticate`): includes `status`, `balance`, `round` (the active bet, if any), and `config` (bet levels, modes, and jurisdiction settings). *** ### POST /wallet/balance Fetches the current player balance. Most other wallet responses already include the balance, so this endpoint is typically used only for periodic polling. **Request body** (`req_Balance`): `{ sessionID }` **Response** (`res_Balance`): `{ balance, status, error }` *** ### POST /wallet/play Triggers a play (debit). The player's balance is reduced by `amount` and the RGS returns the game result in `round.state`. **Request body** (`req_play`): Active session ID. Bet amount as an integer. Currency is an integer — `1000` represents `$10.00`. Multiply the player-facing bet by `API_AMOUNT_MULTIPLIER` before sending. Currency code (e.g. `'USD'`). Bet mode (e.g. `'BASE'`, `'SUPERSPIN'`). Valid modes are defined per-game in the `config` returned by `/wallet/authenticate`. Optional metadata passed through to the game without RGS validation. **Response** (`res_play`): `{ status, balance, round, error }`. The book of events is at `round.state`. *** ### POST /wallet/end-round Closes the active round. No further actions can be made for this round; the RGS sends a payout request to the operator. **Request body** (`req_end_round`): `{ sessionID }` **Response** (`res_end_round`): `{ balance, status, error }` *** ### POST /bet/event Tracks progress through a bet. Sends an event string that is saved to the RGS database alongside the bet. Used by `recordBookEvent` to checkpoint which book event the client has processed. **Request body** (`req_event`): Active session ID. An arbitrary event string. In practice this is the `bookEvent.index` cast to a string. **Response** (`res_event`): `{ event, status, error }` *** ### POST /bet/action Triggers an additional in-round action for games that support them (e.g. a "gamble" or "hold" decision). The `action` field determines whether the RGS sends an additional bet to the operator (`BET`) or routes the request directly to the game (`DECISION`). **Request body** (`req_action`): Active session ID. Action type: `'BET'` or `'DECISION'`. Optional metadata. **Response** (`res_action`): `{ status, balance, action, error }` *** ### POST /session/start Operator-facing endpoint. An operator calls this to start a session and receive a game iframe URL. Send this request to the **mock-casino endpoint** of the environment, not the RGS directly. **Request body** (`req_sess_start`): Currency the player is playing in. A session token from the casino side. Starting balance. Defaults to `100000` (= \$1000.00). When `true`, sets the player's balance to the value in `balance`. Defaults to `false`. **Response** (`res_sess_start`): `{ url, error }` — the `url` is passed into the game iframe. *** ### POST /game/search Searches for game books matching criteria. Used internally for development tooling. **Request body** (`req_search`): `{ mode, search }` where `search` accepts `bookID`, `kind`, `symbol`, `hasWild`, `wildMult`, `gameType`. **Response** (`res_search`): `{ balance, round, error }` ## Error handling When the RGS returns a non-200 HTTP status, `rgsFetcher` logs the error to the console: ```typescript theme={null} if (response.status !== 200) console.error('error', response); ``` It does not throw. The caller receives whatever JSON the server returned. Check `response.status?.statusCode` in the returned data for application-level errors. The `StatusCode` enum covers the following values: | Code | Meaning | | --------- | --------------------------------------------------------- | | `SUCCESS` | Operation completed successfully | | `ERR_SCR` | Invalid secret | | `ERR_OPT` | Invalid operator ID | | `ERR_IPB` | Insufficient player balance | | `ERR_IS` | Invalid session token / session timeout | | `ERR_ATE` | Failed user authentication / authentication token expired | | `ERR_GLE` | Gambling limits exceeded | | `ERR_BNF` | Bet not found | | `ERR_BE` | Player already has an active bet | | `ERR_UE` | General server error (with rollback) | | `ERR_GE` | General server error (without rollback) | ## Usage example The higher-level `rgs-requests` package wraps `rgsFetcher` into named functions. Prefer these over calling `rgsFetcher` directly. ```typescript theme={null} import { requestAuthenticate, requestBet, requestEndRound, requestEndEvent, } from 'rgs-requests'; // 1. Authenticate on game load const auth = await requestAuthenticate({ sessionID, rgsUrl, language: 'en', }); const config = auth.config; // bet levels, modes, jurisdiction flags const activeRound = auth.round; // resume an interrupted round, if any // 2. Place a bet const bet = await requestBet({ sessionID, rgsUrl, currency: 'USD', amount: 1.00, // multiplied by API_AMOUNT_MULTIPLIER before sending mode: 'BASE', }); const bookEvents = bet.round?.state; // the ordered list of game events // 3. Play through all book events await playBookEvents(bookEvents); // 4. Record the final event index await requestEndEvent({ sessionID, rgsUrl, eventIndex: bookEvents.at(-1).index, }); // 5. End the round await requestEndRound({ sessionID, rgsUrl }); ``` ### Calling rgsFetcher directly If you need to call an endpoint not covered by `rgs-requests`, use `rgsFetcher` directly: ```typescript theme={null} import { rgsFetcher } from 'rgs-fetcher'; const result = await rgsFetcher.post({ url: '/bet/action', rgsUrl: 'rgs.example.com', variables: { sessionID: 'abc-123', action: 'DECISION', }, }); ``` ## BetType utility The `rgs-requests` package also exports a `BetType` utility for typing the `round` field of a bet response with your game's specific `BookEvent` union: ```typescript theme={null} import type { BetType } from 'rgs-requests'; import type { BookEvent } from './typesBookEvent'; // round.state is typed as BookEvent[] type Bet = BetType; ``` # Spine Animations Source: https://www.mintlify.com/StakeEngine/web-sdk/api/spine Spine skeleton animation components for character and effect animations ## Overview Spine integration is built on [`@esotericsoftware/spine-pixi-v8`](https://en.esotericsoftware.com/spine-pixi). Load skeleton data via the asset manifest with `type: 'spine'`, then compose the animation with the components below. A typical hierarchy: ``` ``` *** ## \ The root component for a Spine skeleton. Looks up the `SkeletonData` from `stateApp.loadedAssets` by `key`, calculates scale from the declared `width`/`height`, and provides the `Spine` instance to all child components via context. ### Props Asset manifest key for a `spine` asset. The loaded `SkeletonData` is resolved automatically. Desired display width in pixels. When provided, the skeleton is scaled to fit. If only `width` is given, the skeleton is scaled proportionally. Desired display height in pixels. When provided, the skeleton is scaled to fit. If only `height` is given, the skeleton is scaled proportionally. Controls the pivot point of the skeleton. `0` = top-left, `0.5` = centre. Accepts a number or `{ x, y }`. Default `undefined`. Additional scale multiplier applied on top of the width/height-derived scale. Accepts a number or `{ x, y }`. Default `{ x: 1, y: 1 }`. Horizontal position. Default `0`. Vertical position. Default `0`. Opacity from `0` to `1`. Default `1`. Whether the skeleton is rendered. Default `true`. Sort order within the parent. Default `0`. When `true`, logs an error and dumps `loadedAssets` to the console if the key is not found. Useful during development. CSS cursor string. ``, ``, ``, and `` components. `` uses `{#key spineData}` internally — the entire subtree is remounted whenever the resolved skeleton data changes. *** ## \ The lower-level Spine component used internally by ``. Use it directly when you already have a `SPINE_PIXI.SkeletonData` object and do not need the key-lookup and auto-scaling behaviour. ### Props A fully loaded Spine skeleton data object. Passed directly to `new SPINE_PIXI.Spine(spineData)`. Horizontal position. Default `0`. Vertical position. Default `0`. Scale applied to the spine object. Default `1`. Pivot point for rotation and positioning. Opacity. Default `1`. Whether the skeleton is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string. Spine child components. *** ## \ Plays a named animation on a specific track of the parent `` skeleton. Must be a child of `` or ``. When `trackIndex` or `animationName` changes, the previous animation is cleared with `setEmptyAnimation` before the new one starts. ### Props The track index to set the animation on. Track `0` is the base layer; higher indices layer on top. The name of the animation to play, as defined in the Spine editor. Whether the animation loops. Default `false`. Playback speed multiplier for this track. `1` = normal, `2` = double speed. Inherited from `SPINE_PIXI.TrackEntry`. Mix alpha (blend weight) for this track. `1` = full influence. Inherited from `SPINE_PIXI.TrackEntry`. ### Example ```svelte theme={null} ``` *** ## \ Directly manipulates a named bone in the parent skeleton. All props from `SPINE_PIXI.Bone` are accepted and synced reactively. The `y` axis is inverted — `` negates the `y` value before writing it to the bone (`bone.y = -props.y`). This matches the Svelte/PixiJS coordinate system where `y` increases downward. ### Props The name of the bone to control, as defined in the Spine editor. Local x position of the bone. Local y position of the bone (negated before being written to Spine's coordinate system). Local rotation of the bone in degrees (Spine's native unit). Local x scale of the bone. Local y scale of the bone. ### Example ```svelte theme={null} ``` *** ## \ Attaches a container to a named slot in the parent skeleton. Child components are positioned and transformed to follow that slot's world transform each frame. When used inside a ``, the slot's children are hidden automatically when the slot has no active attachment. ### Props The name of the slot to attach to, as defined in the Spine editor. Components to render inside the slot container. They are positioned relative to the slot's world transform. ### Example ```svelte theme={null} ``` *** ## \ Wraps a `` subtree and bridges Spine's `beforeUpdateWorldTransforms` and `afterUpdateWorldTransforms` lifecycle hooks into a PixiJS `EventEmitter`. Required for `` visibility toggling based on slot attachment state. Must be placed inside a ``. ### Props ``, ``, and `` components. ### Example ```svelte theme={null} ``` *** ## Common pattern: GlobalMultiplier A common use case is to scale a Spine skeleton based on a global multiplier (for example, to handle different screen sizes or win-celebration scaling effects). ```svelte theme={null} ``` # Sprite & Text Source: https://www.mintlify.com/StakeEngine/web-sdk/api/sprite-and-text Image rendering, animated sprites, and text display components ## Overview These components render images, sprite sheet animations, and text onto the PixiJS stage. All sprite and text components accept standard PixiJS positioning props (`x`, `y`, `scale`, `rotation`, etc.) inherited from their underlying PIXI display objects. *** ## \ Renders a single static texture loaded from the asset manifest. The texture is looked up by `key` from `stateApp.loadedAssets`. ### Props The key used in the asset manifest (`createApp({ assets })`) to identify this texture. Must correspond to an asset with `type: 'sprite'`. When `true`, logs an error to the console if the texture key is not found in `loadedAssets`. Useful during development. Not normally set directly on `` — the texture is resolved automatically from `key`. Use `BaseSprite` if you need to supply a texture directly. Horizontal position in pixels from the parent origin. Default `0`. Vertical position in pixels from the parent origin. Default `0`. Sets the sprite's origin/pivot. `0` = top-left, `0.5` = centre, `1` = bottom-right. Accepts a uniform number or `{ x, y }`. Default `0`. Uniform or per-axis scale. Default `1`. Rotation in radians. Default `0`. Opacity from `0` to `1`. Default `1`. Whether the sprite is rendered. Default `true`. Sort order within the parent container. Default `0`. Colour tint applied to the texture. Default `0xffffff` (no tint). Apply this sprite as an alpha mask on the parent container. CSS cursor string shown on hover. ### Example ```svelte theme={null} ``` *** ## \ Renders a frame-by-frame animation from an array of textures. Wraps `PIXI.AnimatedSprite`. ### Props Array of textures (or frame objects with `texture` and `time`) that form the animation frames. When `true`, calls `gotoAndPlay(0)`. When `false`, calls `gotoAndStop(0)`. Use this to start and stop the animation declaratively. Default `false`. Playback speed multiplier. `1` = normal speed, `0.5` = half speed. Default `1`. Whether the animation loops. Default `true`. Horizontal position. Default `0`. Vertical position. Default `0`. Origin/pivot point. Default `0` (top-left). Uniform or per-axis scale. Default `1`. Opacity from `0` to `1`. Default `1`. Whether the sprite is rendered. Default `true`. Sort order within the parent. Default `0`. Colour tint applied to all frames. Default `0xffffff`. CSS cursor string. ### Example ```svelte theme={null} ``` *** ## \ A specialised `` that resolves its textures automatically from the asset manifest. Use with assets declared as `type: 'spriteSheet'`. ### Props Asset manifest key for a `spriteSheet` asset. The loaded texture array is passed automatically to the underlying ``. Whether to play the animation. Default `false`. Playback speed multiplier. Default `1`. Whether the animation loops. Default `true`. Horizontal position. Default `0`. Vertical position. Default `0`. Origin/pivot point. Default `0`. Opacity from `0` to `1`. Default `1`. Whether the sprite is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string. ### Example ```svelte theme={null} ``` *** ## \ Renders a text string using a PixiJS canvas-rendered font. Wraps `PIXI.Text`. ### Props The string to display. Text styling options: font family, size, weight, colour, stroke, word wrap, alignment, etc. See the [PixiJS TextStyle docs](https://pixijs.com/8.x/guides/components/text) for all options. Callback fired whenever the rendered size of the text changes — on mount and whenever `text` or `style` changes. Useful for dynamically repositioning elements relative to text bounds. Horizontal position. Default `0`. Vertical position. Default `0`. Origin/pivot point. `0.5` centres the text on `x`/`y`. Default `0`. Uniform or per-axis scale. Default `1`. Opacity from `0` to `1`. Default `1`. Whether the text is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string. ### Example ```svelte theme={null} ``` *** ## \ Renders text using a pre-rasterised bitmap font. Faster than `` for frequently updated strings (scores, counters) because it does not re-rasterise on every change. Requires a `font` asset loaded via the manifest. `` has the same props as ``. The `style.fontFamily` must match the name of a loaded bitmap font. ### Props The string to display. Text style. `fontFamily` must match the loaded bitmap font name. Callback fired on mount and whenever `text` or `style` changes with the current rendered dimensions. Horizontal position. Default `0`. Vertical position. Default `0`. Origin/pivot point. Default `0`. Opacity. Default `1`. Whether the text is rendered. Default `true`. Sort order within the parent. Default `0`. CSS cursor string. ### Example ```svelte theme={null} ``` Use `` over `` for values that update every frame (live counters, timers). Bitmap fonts are rendered once and reused, so repeated text updates are cheaper. # Building and Launching Source: https://www.mintlify.com/StakeEngine/web-sdk/building-and-launching Build your game for production and deploy it to the Stake Engine platform Once your game is working in Storybook, follow these steps to produce a production build, upload it to Stake Engine, and connect a local DEV server to the live RGS for end-to-end testing. Run the build command from the repo root, using TurboRepo's `--filter` flag to target the `lines` game (or replace `lines` with your game's package name): ```bash theme={null} pnpm run build --filter=lines ``` The SvelteKit build produces two output directories inside `apps/lines/.svelte-kit/output/`: | Path | Contents | | ------------------------------ | -------------------------------------------------- | | `prerendered/pages/index.html` | The pre-rendered entry HTML file | | `client/` | All client-side JavaScript, CSS, and static assets | Create a new folder (e.g. `build/`) anywhere on your machine. Copy the `index.html` and the entire contents of the `client/` directory into it so the final structure looks like this: ``` build/ |- index.html |- _app/ |- assets/ |- favicon.svg |- loader.gif |- stake-engine-loader.gif ``` The `index.html` comes from `apps/lines/.svelte-kit/output/prerendered/pages/index.html`. Everything else comes from `apps/lines/.svelte-kit/output/client/`. 1. Log in to [Stake Engine](https://engine.stake.com/). 2. Navigate to the **Files** page of your game. 3. Click **Import** and select your entire `build/` folder to upload the frontend assets. 4. Once the upload completes, click the **Publish Game** button and select **Front End**. 1. Go to the **Developer** page in Stake Engine. 2. Click **Start game session**. 3. Click **Launch in New Tab**. The game opens in a staging environment connected to the live RGS. You can interact with all controls and verify the full round-trip from bet to result. The URL of the launched game contains a query string with the authentication parameters needed to connect to the RGS. Copy the full query string from the browser address bar. Start the local dev server: ```bash theme={null} pnpm run dev --filter=lines ``` Paste the copied query string into the URL of your local dev server. The authentication error screen will resolve and the game will connect to the live RGS, giving you hot-reloading development against a real backend. This workflow is useful for debugging game behaviour that can only be reproduced with real RGS data. For most day-to-day development, Storybook with local book data is faster—see the [Quickstart](/quickstart). ## Rebuilding packages If you modify source files inside `packages/pixi-svelte`, those changes won't take effect automatically because the package is resolved from its compiled output. Rebuild it before running your game: ```bash theme={null} pnpm run build --filter=pixi-svelte ``` This applies to any package that specifies a `main` field pointing to a built artifact in its `package.json`. # Books and BookEvents Source: https://www.mintlify.com/StakeEngine/web-sdk/concepts/book-and-events How the RGS communicates game outcomes through books and bookEvents The RGS (Remote Game Server) communicates the result of every bet as a **book** — a JSON object containing an ordered array of **bookEvents**. Each bookEvent describes one discrete step of the round, and the SDK plays them sequentially to drive the game. ## What is a Book? A book is the JSON payload returned by the RGS for a single bet request. It is randomly selected from millions of pre-computed outcomes produced by the [math SDK](https://stakeengine.github.io/math-sdk/math_docs/general_overview/). Its most important field is `events`: an ordered array of bookEvents that determines everything that happens during the round. ```json theme={null} // base_books.ts — example base-game book { "id": 1, "payoutMultiplier": 0.0, "events": [ { "index": 0, "type": "reveal", "board": [ [{ "name": "L2" }, { "name": "L1" }, { "name": "L4" }, { "name": "H2" }, { "name": "L1" }], [{ "name": "H1" }, { "name": "L5" }, { "name": "L2" }, { "name": "H3" }, { "name": "L4" }], [{ "name": "L3" }, { "name": "L5" }, { "name": "L3" }, { "name": "H4" }, { "name": "L4" }], [{ "name": "H4" }, { "name": "H3" }, { "name": "L4" }, { "name": "L5" }, { "name": "L1" }], [{ "name": "H3" }, { "name": "L3" }, { "name": "L3" }, { "name": "H1" }, { "name": "H1" }] ], "paddingPositions": [216, 205, 195, 16, 65], "gameType": "basegame", "anticipation": [0, 0, 0, 0, 0] }, { "index": 1, "type": "setTotalWin", "amount": 0 }, { "index": 2, "type": "finalWin", "amount": 0 } ], "criteria": "0", "baseGameWins": 0.0, "freeGameWins": 0.0 } ``` The **order of bookEvents matters**. The SDK plays them one after another using `sequence()`, so placing `winInfo` before `reveal` would show win highlights before the reels spin. The math SDK controls this ordering. ## What is a BookEvent? A bookEvent is one element of `book.events`. Every bookEvent has a `type` string field and whatever additional data that event type needs. ```typescript theme={null} // typesBookEvent.ts — example bookEvent types from apps/lines type BookEventReveal = { index: number; type: 'reveal'; board: RawSymbol[][]; paddingPositions: number[]; anticipation: number[]; gameType: GameType; }; type BookEventSetTotalWin = { index: number; type: 'setTotalWin'; amount: number; }; type BookEventWinInfo = { index: number; type: 'winInfo'; totalWin: number; wins: { symbol: SymbolName; kind: number; win: number; positions: Position[]; meta: { lineIndex: number; multiplier: number; winWithoutMult: number; globalMult: number; lineMultiplier: number; }; }[]; }; type BookEventFreeSpinTrigger = { index: number; type: 'freeSpinTrigger'; totalFs: number; positions: Position[]; }; ``` All bookEvent types are collected into a single union type: ```typescript theme={null} // typesBookEvent.ts export type BookEvent = | BookEventReveal | BookEventWinInfo | BookEventSetTotalWin | BookEventFreeSpinTrigger | BookEventUpdateFreeSpin | BookEventCreateBonusSnapshot | BookEventFinalWin | BookEventSetWin | BookEventFreeSpinEnd; export type BookEventOfType = Extract; export type BookEventContext = { bookEvents: BookEvent[] }; ``` ### `BookEventOfType` `BookEventOfType` is a TypeScript utility type that narrows `BookEvent` to the specific type matching the given `type` string. Use it to get full type-safety inside a handler: ```typescript theme={null} reveal: async (bookEvent: BookEventOfType<'reveal'>) => { // bookEvent.board is typed as RawSymbol[][] // bookEvent.gameType is typed as GameType } ``` ## bookEventHandlerMap `bookEventHandlerMap` is an object where each key is a `bookEvent.type` string and each value is an async handler function. When `playBookEvents()` encounters a bookEvent, it looks up the handler by `bookEvent.type` and awaits it. ```typescript theme={null} // apps/lines/src/game/bookEventHandlerMap.ts export const bookEventHandlerMap: BookEventHandlerMap = { reveal: async (bookEvent: BookEventOfType<'reveal'>, { bookEvents }: BookEventContext) => { const isBonusGame = checkIsMultipleRevealEvents({ bookEvents }); if (isBonusGame) { eventEmitter.broadcast({ type: 'stopButtonEnable' }); recordBookEvent({ bookEvent }); } stateGame.gameType = bookEvent.gameType; await stateGameDerived.enhancedBoard.spin({ revealEvent: bookEvent, paddingBoard: config.paddingReels[bookEvent.gameType], }); eventEmitter.broadcast({ type: 'soundScatterCounterClear' }); }, winInfo: async (bookEvent: BookEventOfType<'winInfo'>) => { eventEmitter.broadcast({ type: 'soundOnce', name: 'sfx_winlevel_small' }); await sequence(bookEvent.wins, async (win) => { await animateSymbols({ positions: win.positions }); }); }, setTotalWin: async (bookEvent: BookEventOfType<'setTotalWin'>) => { stateBet.winBookEventAmount = bookEvent.amount; }, updateFreeSpin: async (bookEvent: BookEventOfType<'updateFreeSpin'>) => { eventEmitter.broadcast({ type: 'freeSpinCounterShow' }); eventEmitter.broadcast({ type: 'freeSpinCounterUpdate', current: bookEvent.amount + 1, total: bookEvent.total, }); }, }; ``` Handlers broadcast **emitterEvents** to Svelte components rather than manipulating the DOM directly. This keeps game logic decoupled from the rendering layer. See [Event Emitter](/concepts/event-emitter) for how that works. ## How `playBookEvents()` Works `playBookEvents()` is created by `packages/utils-book/src/createPlayBookUtils.ts`. It iterates the `book.events` array and processes each bookEvent in strict sequence using `sequence()` — an async helper that resolves promises one at a time (unlike `Promise.all()`, which would fire everything at once). ``` book.events └─ [reveal, setTotalWin, winInfo, finalWin] │ ▼ playBookEvents() │ ├─ await playBookEvent(reveal, context) → resolves → next ├─ await playBookEvent(setTotalWin, ctx) → resolves → next ├─ await playBookEvent(winInfo, ctx) → resolves → next └─ await playBookEvent(finalWin, ctx) → resolves → done ``` `playBookEvent()` resolves a single bookEvent by looking up its handler in `bookEventHandlerMap` and awaiting it. It is also used directly in storybook stories (`MODE_BASE/bookEvent/reveal`) to test individual bookEvents in isolation. ## Stateless vs Stateful Games A single RGS request covers the complete round. Slots are the canonical example: one `requestBet` call returns a book, `playBookEvents()` plays it, and the round is done. Stateless games can still be complex — they can have many bookEvent types, multiple game modes, multipliers, and bonus triggers — but everything needed to play the full round is contained in a single book. Multiple RGS requests are needed to complete a round. A mines game is the canonical example: the player reveals tiles one at a time, each reveal is a separate RGS request, and the round is only finished when the player cashes out or hits a mine. For stateful games the `resumeBet` state in the XState machine is essential — it allows the game to re-enter an in-progress round after a page reload. See [Game State Machine](/concepts/state-machine) for details. ## Adding a New BookEvent The recommended workflow for adding a new bookEvent type (e.g. `updateGlobalMult`) is: 1. **Add sample data** to `apps/lines/src/stories/data/bonus_books.ts` and `bonus_events.ts` 2. **Add the story** to `ModeBonusBookEvent.stories.svelte` 3. **Define the type** in `typesBookEvent.ts` and extend the `BookEvent` union 4. **Add the handler** to `bookEventHandlerMap.ts` 5. **Create or update the Svelte component** that subscribes to the emitterEvents the handler broadcasts 6. **Test individually** via `MODE_BONUS/bookEvent/updateGlobalMult` in Storybook 7. **Test in books** via `MODE_BONUS/book/random` in Storybook # Context System Source: https://www.mintlify.com/StakeEngine/web-sdk/concepts/context-system How Svelte context connects apps, packages, and shared state The Web SDK uses [Svelte context](https://svelte.dev/docs/svelte/context) to share typed state between an app and the packages it depends on. Rather than prop-drilling or a global singleton, context is set once at the entry point of the app and read by any descendant component — including components inside `packages/*`. ## Why Svelte Context? Some shared state requires **typed inputs** to create (e.g. the `EmitterEvent` union type, or the `assets` configuration). This makes plain global variables impractical. Svelte context lets the app provide its concrete typed instances, which packages then consume through typed `getContext` helpers — without either side needing to know about the other's internals. Context is hierarchical: only **descendant** components can access a context set by a parent. This is why `setContext()` is called at the entry level of the app. ## Setting Context at the Entry Level In `apps/lines`, context is set once in `context.ts` and called from the entry component: ```typescript theme={null} // apps/lines/src/game/context.ts import { setContextEventEmitter, getContextEventEmitter } from 'utils-event-emitter'; import { setContextXstate, getContextXstate } from 'utils-xstate'; import { setContextLayout, getContextLayout } from 'utils-layout'; import { setContextApp, getContextApp } from 'pixi-svelte'; import { eventEmitter, type EmitterEvent } from './eventEmitter'; import { stateXstate, stateXstateDerived } from './stateXstate'; import { stateLayout, stateLayoutDerived } from './stateLayout'; import { stateApp } from './stateApp'; import { stateGame, stateGameDerived } from './stateGame.svelte'; import { i18nDerived } from '../i18n/i18nDerived'; export const setContext = () => { setContextEventEmitter({ eventEmitter }); setContextXstate({ stateXstate, stateXstateDerived }); setContextLayout({ stateLayout, stateLayoutDerived }); setContextApp({ stateApp }); }; export const getContext = () => ({ ...getContextEventEmitter(), ...getContextLayout(), ...getContextXstate(), ...getContextApp(), stateGame, stateGameDerived, i18nDerived, }); ``` Then in the SvelteKit page entry and in every Storybook story that renders the full game: ```svelte theme={null} ``` If `setContext()` is not called before `` renders, any component or package that calls `getContext()` will throw an error. Storybook stories that render `` must also call `setContext()` first. ## The Four Contexts ### ContextEventEmitter Provides the typed `eventEmitter` instance. The generic type parameter pins the emitter to the game's concrete `EmitterEvent` union, giving full TypeScript intellisense for `broadcast`, `broadcastAsync`, and `subscribeOnMount`. ```typescript theme={null} // set setContextEventEmitter({ eventEmitter }); // get (inside any descendant component) const { eventEmitter } = getContextEventEmitter(); ``` See [Event Emitter](/concepts/event-emitter) for full documentation. *** ### ContextXstate Provides the XState state and its derived helpers. Created by `createXstate()` from `utils-xstate`. ```typescript theme={null} // apps/lines/src/game/stateXstate.ts import { createXstate } from 'utils-xstate'; export const { stateXstate, stateXstateDerived } = createXstate(); ``` | Property | Type | Description | | -------------------- | ----------------------- | ---------------------------------------------------------------- | | `stateXstate` | `{ value: StateValue }` | Svelte `$state` holding the raw XState state value | | `stateXstateDerived` | `object` | Plain functions — `isIdle()`, `isBetting()`, `isPlaying()`, etc. | See [Game State Machine](/concepts/state-machine) for full documentation. *** ### ContextLayout Provides canvas size and layout information. Created by `createLayout()` from `utils-layout`. Because `PIXI.Application` is configured with `resizeTo: window`, the canvas is always the size of the browser window — this context exposes reactive derivations of that. ```typescript theme={null} // packages/utils-layout/src/createLayout.svelte.ts import { innerWidth, innerHeight } from 'svelte/reactivity/window'; const stateLayout = $state({ showLoadingScreen: true, }); const stateLayoutDerived = { canvasSizes, // () => { width, height } — reactive window dimensions canvasRatio, // () => width / height canvasRatioType, // () => 'longWidth' | 'longHeight' | 'almostSquare' canvasSizeType, // () => 'smallMobile' | 'mobile' | 'tablet' | 'largeTablet' | 'desktop' layoutType, // () => 'portrait' | 'landscape' | 'tablet' | 'desktop' isStacked, // () => boolean — true for portrait and almostSquare mainLayout, // () => { x, y, scale, width, height, anchor } mainLayoutStandard, // same as mainLayout but uses the standard 1920×1080 reference sizes normalBackgroundLayout, // ({ scale }) => background positioning object portraitBackgroundLayout, // ({ scale }) => background positioning object }; ``` **Why layout context matters for PixiJS**: Unlike HTML elements which auto-flow, PixiJS requires manual positioning. `canvasSizes` provides the boundaries every component needs to place itself correctly. ```svelte theme={null} ``` `layoutType` breakpoints are derived from the canvas aspect ratio and size: | `layoutType` | Condition | | ------------ | --------------------------------------------------- | | `portrait` | Aspect ratio ≤ 0.8 (tall, narrow — mobile portrait) | | `tablet` | Aspect ratio between 0.8 and 1.3 (near-square) | | `landscape` | Aspect ratio > 1.3 AND small screen width | | `desktop` | Aspect ratio > 1.3 AND large screen width | `isStacked` returns `true` for `portrait` and `almostSquare` layouts, which is useful for stacking UI elements vertically. *** ### ContextApp Provides the PixiJS application state. Created by `createApp()` from `pixi-svelte`. ```typescript theme={null} // apps/lines/src/game/stateApp.ts import { createApp } from 'pixi-svelte'; import assets from './assets'; export const { stateApp } = createApp({ assets }); ``` ```typescript theme={null} // packages/pixi-svelte/src/lib/createApp.svelte.ts const stateApp = $state({ reset, // () => void — resets all fields to initial values assets, // Assets config passed to createApp() loaded: false, // true once PIXI.Assets.load() completes loadingProgress: 0, // 0–100 loading progress loadedAssets: {} as LoadedAssets, // processed asset data ready for pixi-svelte components pixiApplication: undefined as PIXI.Application | undefined, // the live PIXI.Application instance }); ``` | Field | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------ | | `loaded` | `true` after all assets have finished loading. Used to gate the transition from loading screen to game. | | `loadingProgress` | Number from 0–100. Drive a progress bar from this value. | | `loadedAssets` | The result of `PIXI.Assets.load()`. Passed directly to pixi-svelte components like `` and ``. | | `pixiApplication` | The `PIXI.Application` instance. Available after `` mounts. | ## How Packages Consume Context Packages never call `setContext` — they only call `getContext`. For example, `components-layout` uses `getContextLayout()` from `utils-layout`: ```typescript theme={null} // Inside a component in packages/components-layout import { getContextLayout } from 'utils-layout'; const { stateLayout, stateLayoutDerived } = getContextLayout(); ``` This works because the `` from `pixi-svelte` — which is always an ancestor of layout components — was rendered inside a component tree where `setContextLayout()` was already called. The same pattern applies for all four contexts: the app sets them once at the root; any descendant (whether in the app or in a package) reads them with the corresponding `getContext*()` helper. # Event Emitter Source: https://www.mintlify.com/StakeEngine/web-sdk/concepts/event-emitter How the typed event bus connects game logic to Svelte components The `eventEmitter` is a typed event bus that bridges **JavaScript scope** (game logic, bookEvent handlers) and **Svelte component scope** (rendering, animation). Instead of threading reactive state through component props, handlers in `bookEventHandlerMap` broadcast events that any mounted Svelte component can subscribe to. ## Why an Event Emitter? In the Web SDK, game logic lives outside the Svelte component tree — in `bookEventHandlerMap`, `stateGame`, and similar plain TypeScript files. Svelte components handle display and animation. The eventEmitter is the channel between them. This separation means: * Multiple components can react to the same bookEvent independently * Components do not need to know about game logic; they only handle their own emitterEvents * Each emitterEventHandler can be kept small and testable (Single Responsibility Principle) ## The Three Core Methods All three methods are defined in `packages/utils-event-emitter/src/createEventEmitter.ts`: ```typescript theme={null} // packages/utils-event-emitter/src/createEventEmitter.ts export function createEventEmitter() { const subscriptions = new Set(); // Synchronous fire-and-forget. Calls all subscribed handlers immediately. const broadcast = (emitterEvent: TEmitterEvent) => { subscriptions.forEach((emitterEventHandler) => { emitterEventHandler(emitterEvent); }); }; // Async — calls all handlers and returns Promise.all([...]). // Awaiting this pauses the bookEvent handler until every subscriber resolves. const broadcastAsync = (emitterEvent: TEmitterEvent) => { const getPromises = () => Array.from(subscriptions).map((emitterEventHandler) => emitterEventHandler(emitterEvent)); return Promise.all(getPromises()); }; // Registers an emitterEventHandlerMap on component mount. // Automatically unsubscribes when the component is destroyed. const subscribeOnMount = (emitterEventHandlerMap: Partial) => { onMount(() => subscribeHandlerMap(emitterEventHandlerMap)); }; const eventEmitter = { subscribeOnMount, broadcast, broadcastAsync }; return { eventEmitter }; } ``` `broadcastAsync` uses `Promise.all`, meaning all subscribers are started concurrently but the caller waits for **all** of them to resolve before continuing. This is intentional — multiple components may need to finish their animations before the next bookEvent begins. ### When to use `broadcast` vs `broadcastAsync` | Method | Use when | | ---------------- | -------------------------------------------------------------------------------------------------------- | | `broadcast` | The handler is synchronous or the caller doesn't need to wait (show/hide, update counters, play a sound) | | `broadcastAsync` | The caller must wait for the animation or transition to finish before proceeding | ## Defining EmitterEvent Types EmitterEvents are plain discriminated union types. Each variant has a `type` string and any additional data it needs. Define them in the ` ``` Then collect all per-component types into the game-level union in `typesEmitterEvent.ts`: ```typescript theme={null} // apps/lines/src/game/typesEmitterEvent.ts import type { EmitterEventBoard } from '../components/Board.svelte'; import type { EmitterEventFreeSpinCounter } from '../components/FreeSpinCounter.svelte'; import type { EmitterEventFreeSpinIntro } from '../components/FreeSpinIntro.svelte'; import type { EmitterEventWin } from '../components/Win.svelte'; import type { EmitterEventSound } from '../components/Sound.svelte'; // ... etc. export type EmitterEventGame = | EmitterEventBoard | EmitterEventFreeSpinCounter | EmitterEventFreeSpinIntro | EmitterEventWin | EmitterEventSound; ``` Finally, `eventEmitter.ts` composes the game-level type with shared UI types and creates the singleton: ```typescript theme={null} // apps/lines/src/game/eventEmitter.ts import { createEventEmitter } from 'utils-event-emitter'; import type { EmitterEventHotKey } from 'components-shared'; import type { EmitterEventUi } from 'components-ui-pixi'; import type { EmitterEventModal } from 'components-ui-html'; import type { EmitterEventGame } from './typesEmitterEvent'; export type EmitterEvent = | EmitterEventHotKey | EmitterEventUi | EmitterEventModal | EmitterEventGame; export const { eventEmitter } = createEventEmitter(); ``` ## Subscribing in a Component Call `subscribeOnMount` inside the component's ` ``` You only need to provide handlers for the events your component cares about. The `Partial` type means unhandled event types are silently ignored. ## Broadcasting from a BookEvent Handler ```typescript theme={null} // apps/lines/src/game/bookEventHandlerMap.ts updateFreeSpin: async (bookEvent: BookEventOfType<'updateFreeSpin'>) => { // sync — no need to wait for the counter to show eventEmitter.broadcast({ type: 'freeSpinCounterShow' }); eventEmitter.broadcast({ type: 'freeSpinCounterUpdate', current: bookEvent.amount + 1, total: bookEvent.total, }); }, freeSpinTrigger: async (bookEvent: BookEventOfType<'freeSpinTrigger'>) => { // async — wait for the intro animation to fully complete await eventEmitter.broadcastAsync({ type: 'freeSpinIntroUpdate', totalFreeSpins: bookEvent.totalFs, }); }, ``` And the async subscriber in `FreeSpinIntro.svelte`: ```svelte theme={null} context.eventEmitter.subscribeOnMount({ freeSpinIntroUpdate: async (emitterEvent) => { freeSpinsFromEvent = emitterEvent.totalFreeSpins; // pause here until the animation fires its oncomplete callback await waitForResolve((resolve) => (oncomplete = resolve)); }, }); ``` ## Task Breakdown Pattern For complex bookEvents, split the work into multiple fine-grained emitterEvents rather than one large handler. This keeps each handler small, and makes individual steps independently testable in Storybook (`COMPONENTS//emitterEvent`). ```typescript theme={null} // bookEventHandlerMap.ts — tumbleBoard broken into atomic steps tumbleBoard: async (bookEvent: BookEventOfType<'tumbleBoard'>) => { eventEmitter.broadcast({ type: 'tumbleBoardShow' }); eventEmitter.broadcast({ type: 'tumbleBoardInit', addingBoard: bookEvent.newSymbols }); await eventEmitter.broadcastAsync({ type: 'tumbleBoardExplode', explodingPositions: bookEvent.explodingSymbols, }); eventEmitter.broadcast({ type: 'tumbleBoardRemoveExploded' }); await eventEmitter.broadcastAsync({ type: 'tumbleBoardSlideDown' }); eventEmitter.broadcast({ type: 'boardSettle', board: stateGameDerived .tumbleBoardCombined() .map((tumbleReel) => tumbleReel.map((tumbleSymbol) => tumbleSymbol.rawSymbol)), }); eventEmitter.broadcast({ type: 'tumbleBoardReset' }); eventEmitter.broadcast({ type: 'tumbleBoardHide' }); }, ``` EmitterEvents from a single bookEvent can be handled by **different Svelte components**. For example, `tumbleBoardExplode` might be handled by `TumbleBoard.svelte` while `boardSettle` is handled by `Board.svelte`. This is the primary way the SDK achieves decoupled, composable game logic. # Game State Machine Source: https://www.mintlify.com/StakeEngine/web-sdk/concepts/state-machine XState-powered finite state machine for managing bet flows and game states The Web SDK uses [XState](https://stately.ai/docs/) to model the game lifecycle as a **finite state machine**. Rather than scattering `if/else` guards across the codebase, all bet-flow transitions are encoded in one place and every component can query the current state through a small set of derived functions. ## Why a State Machine? Betting logic involves many interlocking conditions: is a bet already in flight? Is auto-bet running? Is there an unfinished round to resume? Without a state machine, these checks accumulate into fragile `if/else` chains. XState makes valid transitions explicit and prevents impossible states (e.g. starting a new bet while one is already running). ## The Game States The machine is created in `packages/utils-xstate/src/createGameActor.ts`. It has five states: ```typescript theme={null} // packages/utils-xstate/src/constants.ts export const STATE_RENDERING = 'rendering' as const; export const STATE_IDLE = 'idle' as const; export const STATE_BET = 'bet' as const; export const STATE_AUTOBET = 'autoBet' as const; export const STATE_RESUME_BET = 'resumeBet' as const; ``` ``` ┌────────────┐ (initial) ──▶ │ rendering │ ──RENDERED──▶ ┌──────┐ └────────────┘ │ idle │ └──┬───┘ ┌────────────────┐ │ ┌────────────────┐ │ resumeBet │◀─┤ │ bet │ │ (fromPromise) │ │ │ (fromPromise) │ └───────┬────────┘ │ └───────┬────────┘ │ │ │ │ ┌────────────────┐ │ └─▶│ autoBet │◀─┘ │ (fromPromise) │ └───────┬────────┘ │ (onDone) │ idle ◀────────────────── ``` The initial state. The game stays here while the loading screen is visible and assets are being processed. Once the `RENDERED` event is sent, the machine transitions to `idle`. The machine is waiting for user input. The bet button is enabled. From here the machine can transition to `bet` (single bet), `autoBet` (auto-spin sequence), or `resumeBet` (if an unfinished round was detected at authenticate time). A single bet is in flight. Invokes the `bet` intermediate machine as a child actor. Returns to `idle` when done. An auto-spin sequence is running. Invokes the `autoBet` intermediate machine which counts down `stateBet.autoSpinsCounter`. Returns to `idle` when the counter reaches zero or is cancelled. An unfinished round from a previous session is being replayed. Invokes the `resumeBet` intermediate machine. Returns to `idle` when done. This state is only entered on game load when `stateBet.betToResume` is set. ## Creating the Actor `createGameActor` is a factory that wires the five states together with the intermediate machine actors: ```typescript theme={null} // packages/utils-xstate/src/createGameActor.ts import { setup, createActor } from 'xstate'; const createGameActor = (intermediateMachines: IntermediateMachines) => { const gameMachine = setup({ actors: { bet: intermediateMachines.bet, autoBet: intermediateMachines.autoBet, resumeBet: intermediateMachines.resumeBet, }, }).createMachine({ initial: 'rendering', states: { [STATE_RENDERING]: stateRendering, [STATE_IDLE]: stateIdle, [STATE_BET]: stateBet, [STATE_AUTOBET]: stateAutoBet, [STATE_RESUME_BET]: stateResumeBet, }, }); const gameActor = createActor(gameMachine); return gameActor; }; ``` In `apps/lines`, the actor is created via the `createXstate()` factory: ```typescript theme={null} // apps/lines/src/game/stateXstate.ts import { createXstate } from 'utils-xstate'; export const { stateXstate, stateXstateDerived } = createXstate(); ``` ## Querying State with `stateXstateDerived` `createXstate()` returns two objects: * `stateXstate` — Svelte `$state` holding the raw XState `StateValue` * `stateXstateDerived` — plain functions that call `matchesState()` from xstate ```typescript theme={null} // packages/utils-xstate/src/createXstateUtils.svelte.ts import { matchesState, type StateValue } from 'xstate'; export const createXstate = () => { const stateXstate = $state({ value: '' as StateValue, }); const matchesXstate = (state: string) => matchesState(state, stateXstate.value); const stateXstateDerived = { matchesXstate, isRendering: () => matchesXstate(STATE_RENDERING), isIdle: () => matchesXstate(STATE_IDLE), isBetting: () => matchesXstate(STATE_BET), isAutoBetting: () => matchesXstate(STATE_AUTOBET), isResumingBet: () => matchesXstate(STATE_RESUME_BET), isPlaying: () => !matchesXstate(STATE_RENDERING) && !matchesXstate(STATE_IDLE), }; return { stateXstate, stateXstateDerived }; }; ``` | Function | Returns `true` when | | ----------------- | --------------------------------------------- | | `isRendering()` | The loading screen is active | | `isIdle()` | Ready and waiting for input | | `isBetting()` | A single bet is in progress | | `isAutoBetting()` | An auto-spin sequence is running | | `isResumingBet()` | A previous unfinished round is being resumed | | `isPlaying()` | Any bet state — `!isRendering() && !isIdle()` | ## Using State in Components Access `stateXstateDerived` via context. Because it is built on Svelte `$state`, any component reading it will react to state changes: ```svelte theme={null} ``` ## BET\_TYPE\_METHODS\_MAP and `end-round` Timing The `createPrimaryMachines` factory in `packages/utils-xstate/src/createPrimaryMachines.ts` defines `BET_TYPE_METHODS_MAP`, which controls **when the `end-round` API is called** relative to game animations: ```typescript theme={null} // packages/utils-xstate/src/createPrimaryMachines.ts const BET_TYPE_METHODS_MAP = { // No win: end-round is never called noWin: { newGame: async () => undefined, endGame: async () => undefined, }, // Single-round win: end-round is called immediately after the bet request // (before animations), balance is updated after animations finish singleRoundWin: { newGame: async () => { const endRoundData = await handleRequestEndRound(); if (endRoundData?.balance) { balanceAmountFromApiHolder = endRoundData.balance.amount; } }, endGame: async () => { if (balanceAmountFromApiHolder !== null) { handleUpdateBalance({ balanceAmountFromApi: balanceAmountFromApiHolder }); balanceAmountFromApiHolder = null; } }, }, // Bonus win (multi-round): end-round is called after all animations finish bonusWin: { newGame: async () => undefined, endGame: async () => { const data = await handleRequestEndRound(); if (data?.balance) { handleUpdateBalance({ balanceAmountFromApi: data.balance.amount }); balanceAmountFromApiHolder = null; } }, }, } as const; ``` `end-round` is never called. The round closed itself with no payout. `end-round` is called immediately after the RGS bet response (`newGame`), before any animations run. The balance value is cached and only applied to the UI after animations complete (`endGame`). This allows the round to be resumed if the player refreshes mid-animation. `end-round` is called **after** all animations complete (`endGame`). This is required for multi-request rounds (e.g. free spins) where the round remains `active` until the player finishes the bonus game. Whether a bet can be resumed from the authenticate request is determined by when `end-round` is called. If `end-round` has not been called, `stateBet.betToResume.active` will be `true`, and the machine will enter the `resumeBet` state on next load. # Adding a BookEvent Source: https://www.mintlify.com/StakeEngine/web-sdk/guides/adding-a-book-event Step-by-step guide to implementing a new bookEvent in your game This guide walks through every file you need to touch when adding a new `bookEvent` to your game. We'll use `updateGlobalMult` — a bonus game multiplier feature — as the running example throughout. A **book** is the JSON payload returned by the RGS for each game round. It contains an array of **bookEvents** that are played in sequence. The order of bookEvents matters — `win` should always come after `reveal`, for example. `apps/lines/src/stories/data/bonus_books.ts` holds an array of complete bonus books. The story `MODE_BONUS/book/random` randomly picks from this array to simulate RGS responses. Add a new `updateGlobalMult` event into an existing book's `events` array (or create a new book). Copy the exact shape from your math package output: ```json theme={null} { "type": "updateGlobalMult", "globalMult": 3 } ``` Place it at the right position in the sequence — global multiplier updates typically come before the spin reveal or win events that use the multiplier. `apps/lines/src/stories/data/bonus_events.ts` exports a flat object where each key is a `bookEvent.type`. The per-event stories (`MODE_BONUS/bookEvent/`) use this to run a single bookEvent in isolation. ```typescript theme={null} // bonus_events.ts export default { // ... existing events ... updateGlobalMult: { type: 'updateGlobalMult', globalMult: 3, }, // ... } ``` `apps/lines/src/stories/ModeBonusBookEvent.stories.svelte` defines all sub-stories under `MODE_BONUS/bookEvent`. Add a `` block for your new event: ```svelte theme={null} await playBookEvent(data, { bookEvents: [] }), })} {template} /> ``` After this step you will see `MODE_BONUS/bookEvent/updateGlobalMult` in the Storybook sidebar with an **Action** button. Clicking it does nothing yet — that is expected. You've set up the test harness first. Setting up the story before writing any logic is intentional. It gives you a live environment to verify each implementation step as you go. `apps/lines/src/game/typesBookEvent.ts` contains the `BookEvent` union type. Add a new member type for your bookEvent: ```typescript theme={null} // typesBookEvent.ts type BookEventUpdateGlobalMult = { index: number; type: 'updateGlobalMult'; globalMult: number; }; export type BookEvent = | BookEventReveal | BookEventWinInfo | BookEventSetTotalWin | BookEventFreeSpinTrigger | BookEventUpdateFreeSpin | BookEventFinalWin | BookEventSetWin | BookEventFreeSpinEnd | BookEventUpdateGlobalMult // <-- add here // ... ; ``` `BookEvent` is a **TypeScript union type**. `BookEventOfType` (defined at the bottom of this file) uses `Extract` to narrow the union to a specific member — this is what gives you typed `bookEvent` parameters in handlers. `apps/lines/src/game/bookEventHandlerMap.ts` maps each `bookEvent.type` string to an async handler function. Add a handler for `updateGlobalMult`: ```typescript theme={null} // bookEventHandlerMap.ts export const bookEventHandlerMap: BookEventHandlerMap = { // ... existing handlers ... updateGlobalMult: async (bookEvent: BookEventOfType<'updateGlobalMult'>) => { eventEmitter.broadcast({ type: 'globalMultiplierShow' }); eventEmitter.broadcast({ type: 'globalMultiplierUpdate', multiplier: bookEvent.globalMult, }); }, // ... }; ``` Because you added `BookEventUpdateGlobalMult` to the union in the previous step, TypeScript will provide full intellisense on `bookEvent` — including `bookEvent.globalMult`. The handler broadcasts **emitterEvents** via `eventEmitter.broadcast()` (sync, fire-and-forget) or `eventEmitter.broadcastAsync()` (async, waits for all subscribers to resolve). Use `broadcastAsync` when you need to wait for an animation to complete before proceeding. Create `apps/lines/src/components/GlobalMultiplier.svelte`. All logic related to the global multiplier should live here. Start by declaring the `EmitterEventGlobalMultiplier` union type in the module script block. This type is exported so other files can import it: ```svelte theme={null} ``` `EmitterEventGlobalMultiplier` is a **TypeScript union type**, just like `BookEvent`. Each member corresponds to one atomic action this component can respond to. `apps/lines/src/game/typesEmitterEvent.ts` assembles the `EmitterEventGame` union from all per-component exports: ```typescript theme={null} // typesEmitterEvent.ts import type { EmitterEventBoard } from '../components/Board.svelte'; import type { EmitterEventBoardFrame } from '../components/BoardFrame.svelte'; import type { EmitterEventFreeSpinIntro } from '../components/FreeSpinIntro.svelte'; import type { EmitterEventFreeSpinCounter } from '../components/FreeSpinCounter.svelte'; import type { EmitterEventFreeSpinOutro } from '../components/FreeSpinOutro.svelte'; import type { EmitterEventWin } from '../components/Win.svelte'; import type { EmitterEventSound } from '../components/Sound.svelte'; import type { EmitterEventTransition } from '../components/Transition.svelte'; import type { EmitterEventGlobalMultiplier } from '../components/GlobalMultiplier.svelte'; // <-- add export type EmitterEventGame = | EmitterEventBoard | EmitterEventBoardFrame | EmitterEventWin | EmitterEventFreeSpinIntro | EmitterEventFreeSpinCounter | EmitterEventFreeSpinOutro | EmitterEventSound | EmitterEventTransition | EmitterEventGlobalMultiplier; // <-- add ``` `apps/lines/src/game/eventEmitter.ts` composes the top-level `EmitterEvent` union and creates the singleton `eventEmitter` instance: ```typescript theme={null} // eventEmitter.ts import type { EmitterEventGame } from './typesEmitterEvent'; export type EmitterEvent = EmitterEventUi | EmitterEventHotKey | EmitterEventGame; export const { eventEmitter } = createEventEmitter(); ``` Because `EmitterEventGame` now includes `EmitterEventGlobalMultiplier`, all `eventEmitter.broadcast()` calls in the codebase will now offer `globalMultiplierShow`, `globalMultiplierHide`, and `globalMultiplierUpdate` as valid types in the intellisense autocomplete. Back in `GlobalMultiplier.svelte`, add the component script and subscribe to the emitter events: ```svelte theme={null} ``` `subscribeOnMount` automatically registers the handlers when the component mounts and unregisters them on destroy, so you don't need to manage lifecycle manually. Each handler in the map follows the Single Responsibility Principle — `globalMultiplierShow` only shows the component, `globalMultiplierUpdate` only updates the value. See the [Task Breakdown Pattern](/guides/task-breakdown) guide for why this matters. Run Storybook and navigate to `MODE_BONUS/bookEvent/updateGlobalMult`: ```bash theme={null} pnpm run storybook --filter=lines ``` Click the **Action** button. The `GlobalMultiplier` component should animate correctly. When the handler completes, the story displays: > ⓘ Action is resolved ✅ If the action does not resolve, go back to the component and debug until it resolves cleanly. If the component is hard to debug through emitterEvents alone, create a dedicated story `COMPONENTS/GlobalMultiplierSpine/component` that accepts the multiplier value as a Storybook control prop. This separates visual testing from event-driven testing. Switch to `MODE_BONUS/book/random` in the Storybook sidebar. Keep clicking **Action** to cycle through the books defined in `bonus_books.ts`. Because you added the `updateGlobalMult` event to one of those books in step 1, it will appear eventually — and your component should respond correctly. Once every bookEvent story resolves with ✅ and the full book plays through without issues, the feature is complete. ## Reference: existing handler example For comparison, here is the `updateFreeSpin` handler from `bookEventHandlerMap.ts` — a simpler real-world example showing the same pattern: ```typescript theme={null} updateFreeSpin: async (bookEvent: BookEventOfType<'updateFreeSpin'>) => { eventEmitter.broadcast({ type: 'freeSpinCounterShow' }); stateUi.freeSpinCounterShow = true; eventEmitter.broadcast({ type: 'freeSpinCounterUpdate', current: bookEvent.amount + 1, total: bookEvent.total, }); stateUi.freeSpinCounterCurrent = bookEvent.amount + 1; stateUi.freeSpinCounterTotal = bookEvent.total; }, ``` And the corresponding subscriber in `FreeSpinCounter.svelte`: ```svelte theme={null} ``` # Testing with Storybook Source: https://www.mintlify.com/StakeEngine/web-sdk/guides/storybook-testing How to use Storybook to test game components, bookEvents, and full game modes Storybook is the primary testing tool for Web SDK games. It lets you test the whole game, intermediate components, and atomic components — from a single Svelte component rendered in isolation, to a single bookEvent animation, to a full multi-spin bonus mode book. ## Running Storybook ```bash theme={null} pnpm run storybook --filter=lines ``` Replace `lines` with the `name` field from the target app's `package.json`. For example, if you're working on a `cluster` game, use `--filter=cluster`. On Windows, initial Storybook startup can take up to 15 minutes. Once loaded, hot reloading is fast. The recommended approach is to implement each piece of a game in Storybook using task breakdown — test one emitterEvent at a time rather than waiting for a full build to verify things. For Windows users, you may need to add `cross-env` to the storybook npm script in `package.json`: ```json theme={null} "storybook": "cross-env PUBLIC_CHROMATIC=true storybook dev -p 6001 public" ``` ## Three types of stories ### 1. `COMPONENTS//component` Tests a single Svelte component in complete isolation. Useful for components that accept props directly, like `Symbol` or `Board`. Examples from the `lines` app: * `COMPONENTS/Game/component` — renders the full `` component including the loading screen * `COMPONENTS/Symbol/component` — renders `` with Storybook controls for state * `COMPONENTS/Symbol/symbols` — renders all symbols in all states at once These stories are defined in files like `ComponentsGame.stories.svelte` and `ComponentsSymbol.stories.svelte`. ### 2. `MODE_/bookEvent/` Tests a single bookEvent in isolation. An **Action** button appears in the corner of the canvas. Clicking it runs `playBookEvent()` with the corresponding test data from the events file. Examples: * `MODE_BASE/bookEvent/reveal` — spins the reels with a base game board * `MODE_BASE/bookEvent/freeSpinTrigger` — plays the scatter animation and free spin intro * `MODE_BONUS/bookEvent/updateFreeSpin` — updates the free spin counter * `MODE_BONUS/bookEvent/freeSpinEnd` — plays the free spin outro and transition back When the action completes successfully, the story displays: > ⓘ Action is resolved ✅ If this message does not appear, the bookEventHandler has not resolved — there is a bug to fix before continuing. ### 3. `MODE_/book/random` Tests a full game book — a complete sequence of bookEvents as the RGS would return them. The Action button runs `playBookEvents()` which plays each event in the book one after another using `sequence()`. On each click, a random book is selected from the corresponding `*_books.ts` data file. Keep clicking to cycle through different scenarios: no-win spins, small wins, big wins, bonus triggers. ## Story file structure The `lines` app has these story files in `apps/lines/src/stories/`: ``` stories/ ├── ComponentsGame.stories.svelte # COMPONENTS/Game/* ├── ComponentsSymbol.stories.svelte # COMPONENTS/Symbol/* ├── data/ │ ├── base_books.ts # Books for MODE_BASE/book/random │ ├── base_events.ts # Events for MODE_BASE/bookEvent/* │ ├── bonus_books.ts # Books for MODE_BONUS/book/random │ └── bonus_events.ts # Events for MODE_BONUS/bookEvent/* ├── ModeBaseBook.stories.svelte # MODE_BASE/book/* ├── ModeBaseBookEvent.stories.svelte # MODE_BASE/bookEvent/* ├── ModeBonusBook.stories.svelte # MODE_BONUS/book/* └── ModeBonusBookEvent.stories.svelte # MODE_BONUS/bookEvent/* ``` ## How to add a new story Open the appropriate `ModeBookEvent.stories.svelte` file. The pattern is consistent across all story files: ```svelte theme={null} {#snippet template(args: TemplateArgs)} { await args.action?.(args.data); }} > {/snippet} ``` Then add a `` block for your new event: ```svelte theme={null} await playBookEvent(data, { bookEvents: [] }), })} {template} /> ``` * `name` becomes the story path: `MODE_BONUS/bookEvent/updateGlobalMult` * `data` is the bookEvent object from the events file * `action` is the function that runs when you click the **Action** button * `skipLoadingScreen: true` skips the asset loading screen for faster iteration ## The Action button and resolution The Action button is rendered by `StoryGameTemplate` from `components-storybook`. It calls the `action` function you provide in `args`. For bookEvent stories, the action calls `playBookEvent(data, context)`, which looks up the handler in `bookEventHandlerMap` by `data.type` and runs it. For book stories, the action calls `playBookEvents(book)`, which runs each event in the book's `events` array in sequence using `sequence()` — one after another, not all at once. The **ⓘ Action is resolved ✅** message appears when the `action` Promise resolves. For complex bookEvents with animations, this happens after all `broadcastAsync` calls have completed. ## Best practices Implement each feature step by step in Storybook using the task breakdown pattern. Test each emitterEvent individually before integrating into a full book test. This approach catches issues early and makes debugging much easier. * Build the story first, then implement the handler. The story gives you immediate feedback. * Use `COMPONENTS//component` stories for pure visual testing of a component's props. * Use `MODE_/bookEvent/` stories during development to verify each bookEvent works. * Use `MODE_/book/random` stories as the final integration test before marking a feature done. * If a component is hard to debug through emitterEvents, create a `COMPONENTS//emitterEvent` story that sends a single emitterEvent directly via controls. # Task Breakdown Pattern Source: https://www.mintlify.com/StakeEngine/web-sdk/guides/task-breakdown How to decompose complex bookEvents into atomic, testable emitterEvents Task Breakdown is the central design principle of the Web SDK. Rather than implementing a complex game feature as one large block, you decompose it into small, named, independently-testable pieces called **emitterEvents**. ## The core principle Each `emitterEventHandler` should do one minimal job — the job described by its type name. This is the [Single Responsibility Principle](https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design#single-responsibility-principle) applied to game events. For example, `freeSpinCounterShow` should only show the component. It should not update the counter value. `freeSpinCounterUpdate` should only update the counter values. It should not show or hide anything. When each handler does exactly one thing: * It can be named precisely, making the sequence of operations readable * Each step can be tested independently in Storybook * Bugs are isolated to a single handler instead of buried in a large function * Components stay focused and easy to reason about ## The tumbleBoard example `tumbleBoard` is a mechanically complex bookEvent. Symbols explode, disappear, new symbols fall from above, and the board settles into a new state. A naive implementation might put all of this logic in a single `tumbleBoard` emitterEvent. Instead, it is broken into four atomic steps: | emitterEvent | Responsibility | | --------------------------- | ------------------------------------------------------ | | `tumbleBoardInit` | Load new symbols into the board above the visible area | | `tumbleBoardExplode` | Play explosion animations on the winning symbols | | `tumbleBoardRemoveExploded` | Remove the exploded symbols from state | | `tumbleBoardSlideDown` | Animate remaining symbols sliding down to fill gaps | The `bookEventHandler` in `bookEventHandlerMap.ts` sequences them: ```typescript theme={null} // bookEventHandlerMap.ts tumbleBoard: async (bookEvent: BookEventOfType<'tumbleBoard'>) => { eventEmitter.broadcast({ type: 'tumbleBoardShow' }); eventEmitter.broadcast({ type: 'tumbleBoardInit', addingBoard: bookEvent.newSymbols }); await eventEmitter.broadcastAsync({ type: 'tumbleBoardExplode', explodingPositions: bookEvent.explodingSymbols, }); eventEmitter.broadcast({ type: 'tumbleBoardRemoveExploded' }); await eventEmitter.broadcastAsync({ type: 'tumbleBoardSlideDown' }); eventEmitter.broadcast({ type: 'boardSettle', board: stateGameDerived .tumbleBoardCombined() .map((tumbleReel) => tumbleReel.map((tumbleSymbol) => tumbleSymbol.rawSymbol)), }); eventEmitter.broadcast({ type: 'tumbleBoardReset' }); eventEmitter.broadcast({ type: 'tumbleBoardHide' }); }, ``` `eventEmitter.broadcast()` is synchronous and fire-and-forget. `eventEmitter.broadcastAsync()` waits for all subscribers to resolve before continuing. Use `broadcastAsync` for any step that involves an animation you need to wait on — like the explosion and slide-down phases above. The corresponding `subscribeOnMount` in `TumbleBoard.svelte` lists all the handlers: ```svelte theme={null} context.eventEmitter.subscribeOnMount({ tumbleBoardShow: () => {}, tumbleBoardHide: () => {}, tumbleBoardInit: () => {}, tumbleBoardReset: () => {}, tumbleBoardExplode: () => {}, tumbleBoardRemoveExploded: () => {}, tumbleBoardSlideDown: () => {}, }); ``` At a glance you can read the full lifecycle of a tumble from the handler map alone — no need to open `TumbleBoard.svelte` to understand the sequence. ## Why this matters for testing Because every step is its own named emitterEvent, you can write a Storybook story for each one under `COMPONENTS//emitterEvent`. You can test that the explosion animation works before the slide-down is even implemented. You can verify the slide-down independently if the explosion is broken. With a monolithic approach you would have to test all or nothing. ## emitterEvents can span multiple components The emitterEvents under a single bookEvent can come from different Svelte components. A `reveal` bookEvent might broadcast events that are handled by `Board.svelte`, `Sound.svelte`, and `Transition.svelte` simultaneously. This is intentional. The `bookEventHandler` is the orchestrator. Individual Svelte components are the workers. Each component only knows about its own slice of the work. Visually this looks like a bookEvent with coloured blocks beneath it — each colour representing a different Svelte component that handles some of the emitterEvents in that sequence. ## Stateless vs stateful games Task Breakdown applies regardless of game type, but it is especially valuable for stateless games: * **Stateless games**: A single request to the RGS produces all the data for a complete round. A slots game is a typical example — one request returns the full `book` of `bookEvents` for a spin. * **Stateful games**: Multiple requests are needed to complete a round. A mines game (where the player reveals tiles one at a time) requires a separate RGS request for each tile reveal. Stateless games can still be surprisingly complex — multiple game modes, free spin mechanics, global multipliers, win levels, and different spin types all need to be decomposed and tested individually. ## Implementation workflow The recommended way to build any feature is to implement it incrementally through the task breakdown: 1. Define the bookEvent type 2. Write the bookEventHandler with the emitterEvent sequence 3. Define emitterEvent types on the Svelte component 4. Implement one emitterEventHandler at a time 5. Test each one in Storybook before moving to the next This matches the steps described in the [Adding a BookEvent](/guides/adding-a-book-event) guide. # UI Components and Internationalisation Source: https://www.mintlify.com/StakeEngine/web-sdk/guides/ui-and-i18n Using the built-in UI package and adding multi-language support ## UI packages The SDK provides two UI packages: * **`components-ui-pixi`** (`packages/components-ui-pixi`) — PixiJS-based UI components rendered on the canvas. Handles layout, buttons, labels, and the main `` component. * **`components-ui-html`** (`packages/components-ui-html`) — HTML/Svelte components for overlay UI: modals, pay table, auto-spin settings, bet menu, settings, and game rules. Both packages are functional and include features like auto-gaming, turbo mode, bonus buy, and responsive layout. They are a starting point, not a final product. The UI packages are designed to be used as a reference and starting point. For your final game, customise them to match your branding or replace them entirely. The source code is fully open. ## The `` component `` from `components-ui-pixi` is the main canvas UI wrapper. It accepts two Svelte 5 snippet props: | Prop | Description | | ---------- | -------------------------------------------- | | `gameName` | Renders the game name label in the UI layout | | `logo` | Renders a logo or branding element | Here is the full usage example from the `lines` app: ```svelte theme={null} {#snippet gameName()} {/snippet} {#snippet logo()} {/snippet} {#snippet version()} {/snippet} ``` ## components-ui-pixi components All components live in `packages/components-ui-pixi/src/components/`: **Buttons** | Component | Purpose | | ----------------------------------- | ----------------------------- | | `ButtonBet` | Main bet/spin button | | `ButtonAutoSpin` | Toggle auto-spin mode | | `ButtonBuyBonus` | Purchase the bonus feature | | `ButtonMenu` | Open the game menu | | `ButtonTurbo` | Toggle turbo mode | | `ButtonSettings` | Open settings modal | | `ButtonPayTable` | Open pay table modal | | `ButtonGameRules` | Open game rules modal | | `ButtonSoundSwitch` | Toggle sound on/off | | `ButtonDecrease` / `ButtonIncrease` | Adjust bet amount | | `ButtonDrawer` | Collapse/expand the UI drawer | **Labels** | Component | Purpose | | ---------------------- | -------------------------------------- | | `LabelBalance` | Displays current balance | | `LabelBet` | Displays current bet amount | | `LabelWin` | Displays current win amount | | `LabelFreeSpinCounter` | Displays free spin count in the UI bar | **Layout** The `` component selects a layout based on device type: | Component | When used | | ----------------- | ------------------------- | | `LayoutDesktop` | Widescreen desktop | | `LayoutLandscape` | Landscape phone or tablet | | `LayoutPortrait` | Portrait phone | | `LayoutTablet` | Tablet aspect ratio | ## components-ui-html components All modal and overlay components live in `packages/components-ui-html/src/components/`: * `Modals` — container for all overlay modals * `ModalAutoSpin` — auto-spin configuration modal * `ModalBetMenu` — bet amount selection * `ModalBuyBonus` — bonus buy flow with `ModalBuyBonusConfirm` * `ModalPayTable` — pay table display * `ModalGameRules` — game rules display * `ModalSettings` — game settings including sound * `ModalError` — error display * `GameVersion` — displays the game version string ## Internationalisation The SDK uses [Lingui](https://www.npmjs.com/package/@lingui/core) for internationalisation. Configuration lives in `packages/config-lingui`. ### How translations work The `i18nDerived` object in `packages/components-ui-pixi/src/i18n/i18nDerived.ts` provides reactive translation functions for all UI strings: ```typescript theme={null} // packages/components-ui-pixi/src/i18n/i18nDerived.ts import { stateI18nDerived, stateUrlDerived } from 'state-shared'; export const i18nDerived = { audio: () => stateI18nDerived.translate('AUDIO'), balance: () => stateI18nDerived.translate('BALANCE'), win: () => stateI18nDerived.translate('WIN'), bet: () => stateUrlDerived.social() ? 'SPIN' : stateI18nDerived.translate('BET'), stop: () => stateI18nDerived.translate('STOP'), buyBonus: () => stateUrlDerived.social() ? 'PLAY BONUS' : stateI18nDerived.translate('BUY BONUS'), disable: () => stateI18nDerived.translate('DISABLE'), freeSpins: () => stateI18nDerived.translate('FREE SPINS'), decrease: () => stateI18nDerived.translate('-'), increase: () => stateI18nDerived.translate('+'), menu: () => stateI18nDerived.translate('MENU'), turbo: () => stateI18nDerived.translate('TURBO'), autoSpin: () => stateI18nDerived.translate('AUTO SPIN'), payTable: () => stateI18nDerived.translate('PAYTABLE'), info: () => stateI18nDerived.translate('INFO'), settings: () => stateI18nDerived.translate('SETTINGS'), soundOn: () => stateI18nDerived.translate('SOUND ON'), soundOff: () => stateI18nDerived.translate('SOUND OFF'), menuExit: () => stateI18nDerived.translate('EXIT'), }; ``` Message catalogues for each locale live in `packages/components-ui-pixi/src/i18n/messagesMap/`. ### Social casino mode When a game runs on a social casino platform (such as stake.us), the query string contains `social=true`. The `stateUrlDerived.social()` function checks for this flag. Some labels must change for social casino compliance: | Standard casino | Social casino | | --------------- | ------------- | | BET | SPIN | | BUY BONUS | PLAY BONUS | This is already handled in `i18nDerived.ts` as shown above — the `bet` and `buyBonus` functions branch based on `stateUrlDerived.social()`. If you add custom UI text that needs a social casino variant, follow the same pattern: check `stateUrlDerived.social()` and return the alternative string. ### Currency formatting Use the `numberToCurrencyString` function from `packages/utils-shared/amount.ts` to format currency values. This function delegates to Lingui's `i18n.number` under the hood, so any currency supported by the Intl API is automatically handled. The player's currency is set from the authentication response: ```typescript theme={null} stateBet.currency = authenticateData.balance.currency; ``` **Special currencies**: Some social casino platforms (such as stake.us) use non-standard currency codes that the Intl API does not recognise. These are handled by `NO_LOCALISATION_CURRENCY_MAP` in `packages/utils-shared/amount.ts`, which maps those codes to custom formatting rules. # Introduction Source: https://www.mintlify.com/StakeEngine/web-sdk/introduction What is the Stake Engine Web SDK and how it helps you build casino games The Stake Engine Web SDK is an open-source monorepo framework for building casino games that run on the [Stake Engine](https://engine.stake.com/) Remote Game Server (RGS). It gives you a fully wired-up development environment—complete with component isolation, a local test harness, and a one-command build pipeline—so you can focus on the game itself rather than the infrastructure around it. You can use the SDK in two ways: * **Deploy on Stake Engine** — build a game using the provided patterns and launch it directly on the platform. * **Starting point for custom implementations** — fork the repo and modify any part of the source code. You have 100% freedom over the codebase. ## Key technologies The SDK is powered by the following npm packages: | Technology | Purpose | | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | [Svelte 5](https://www.npmjs.com/package/svelte) | Reactive UI framework for game components | | [PixiJS 8](https://www.npmjs.com/package/pixi.js) | WebGL-accelerated 2D rendering | | [pixi-svelte](https://www.npmjs.com/package/pixi-svelte) | In-house package combining PixiJS and Svelte for declarative canvas rendering | | [SvelteKit](https://www.npmjs.com/package/@sveltejs/kit) | Application framework and build pipeline | | [TurboRepo](https://www.npmjs.com/package/turbo) | Monorepo task orchestration | | [XState](https://www.npmjs.com/package/xstate) | Finite state machine for bet lifecycle management | | [Storybook](https://www.npmjs.com/package/storybook) | Component isolation and game testing environment | | [Lingui](https://www.npmjs.com/package/@lingui/core) | Internationalisation | | [TypeScript](https://www.npmjs.com/package/typescript) | Type safety across the monorepo | | [pnpm](https://www.npmjs.com/package/pnpm) | Package manager with workspace support | ## The Stake Engine platform Stake Engine (`engine.stake.com`) is the RGS that sits behind every game. When a player places a bet, the RGS returns a **book**—a JSON object containing a sequence of **bookEvents** that fully describes how that round plays out. The SDK's architecture is built around consuming those bookEvents in order. ## Architecture overview The data flow from RGS to screen follows a single, consistent pipeline: ``` RGS → book → bookEvents → bookEventHandlerMap → emitterEvents → Svelte components ``` 1. **RGS** returns a book for each game request. 2. **`playBookEvents()`** iterates through `book.events` in sequence—order matters because it determines the visual behaviour (e.g. spin before win). 3. Each **`bookEventHandler`** in `bookEventHandlerMap` processes one event type. It typically broadcasts one or more **emitterEvents** via `eventEmitter`. 4. **Svelte components** subscribe to emitterEvents with `eventEmitter.subscribeOnMount()` and handle them synchronously or asynchronously—waiting for animations to finish before the sequence continues. This event-driven architecture (see the [bookEvent and emitterEvent concepts](/concepts/book-and-events)) keeps game logic decoupled from rendering and makes every step independently testable in Storybook. ## Monorepo structure The repository follows a standard TurboRepo layout: ``` root |_apps # Individual game applications | |_cluster | |_lines | |_number-picker | |_price | |_scatter | |_ways | |_packages # Shared modules |_config-* |_constants-* |_state-* |_utils-* |_components-* |_pixi-* ``` **`/apps`** — each folder is a complete, runnable game. The `lines` game is the primary reference implementation used throughout this documentation. **`/packages`** — shared modules consumed by apps and other packages via `workspace:*` dependencies. They follow a `-` naming convention (e.g. `utils-book`, `components-ui-pixi`). Get the SDK running locally in minutes with Storybook. Understand the bookEvent and emitterEvent architecture. Explore the shared packages available across the monorepo. # Packages Overview Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/overview All local packages in the Stake Engine Web SDK monorepo The Stake Engine Web SDK is organized as a [TurboRepo](https://turbo.build/repo/docs) monorepo. All game apps live in `/apps`, and all reusable code lives in `/packages`. Apps and packages can consume any sibling package directly without publishing to npm — the dependency is declared with `workspace:*` in `package.json`. ```json theme={null} // apps/lines/package.json { "name": "lines", "dependencies": { "pixi-svelte": "workspace:*", "constants-shared": "workspace:*", "state-shared": "workspace:*", "utils-shared": "workspace:*", "components-shared": "workspace:*" }, "devDependencies": { "config-ts": "workspace:*" } } ``` ## Naming convention Every package name follows the pattern `-`. For example, `components-pixi` is a components package whose special dependency is `pixi-svelte`, and `utils-sound` is a utils package whose special dependency is [howler](https://www.npmjs.com/package/howler). `*-shared` packages are an exception: instead of a special dependency they are designed to have a **minimum set of dependencies** and a **broad set of use cases** so they can be imported almost everywhere. ## Monorepo structure ``` root ├── apps/ │ ├── cluster/ │ ├── lines/ │ ├── price/ │ ├── scatter/ │ └── ways/ └── packages/ ├── config-* ├── constants-* ├── state-* ├── utils-* ├── components-* ├── pixi-* ├── rgs-fetcher/ └── rgs-requests/ ``` ## Package groups Reusable toolchain configurations shared across apps and packages. * `config-lingui` — [lingui](https://www.npmjs.com/package/@lingui/core) i18n config * `config-storybook` — [Storybook](https://www.npmjs.com/package/storybook) config * `config-svelte` — [Svelte](https://www.npmjs.com/package/svelte) compiler config * `config-ts` — [TypeScript](https://www.npmjs.com/package/typescript) `tsconfig` base * `config-vite` — [Vite](https://www.npmjs.com/package/vite) config Svelte 5 + PixiJS 8 integration layer. * `pixi-svelte` — declarative Svelte components wrapping PixiJS primitives; also published to [npm](https://www.npmjs.com/package/pixi-svelte) * `pixi-svelte-storybook` — Storybook for `pixi-svelte` components Global compile-time constants with no runtime dependencies. * `constants-shared` — bet multipliers, z-index values, time helpers, color constants, particle configs Global reactive Svelte `$state` objects shared across packages. * `state-shared` — `stateBet`, `stateSound`, `stateUrl`, `stateModal`, `stateMeta`, `stateConfig`, `stateUi`, `stateI18n` Utility functions and types organized by dependency or use case. * `utils-book` — book event processing (`playBookEvents`, `playBookEvent`) * `utils-event-emitter` — type-safe event bus (`createEventEmitter`) * `utils-xstate` — XState finite state machine for bet flows * `utils-layout` — responsive canvas layout system * `utils-slots` — reel and board spin mechanics * `utils-sound` — audio management via Howler.js * `utils-fetcher` — wrapper around the Fetch API * `utils-shared` — miscellaneous helpers (no lodash/lingui dependency) * `utils-resize-observer` — ResizeObserver utilities * `utils-bet` — bet-related types and helpers Reusable Svelte components organized by rendering layer. * `components-layout` — layout components (depend on `utils-layout`) * `components-pixi` — PixiJS game components (depend on `pixi-svelte`) * `components-shared` — HTML-based shared components * `components-storybook` — Storybook helper components * `components-ui-pixi` — PixiJS UI components (bet button, balance, etc.) * `components-ui-html` — HTML UI components (modals, version overlay, etc.) Remote Game Server (RGS) communication layer. * `rgs-fetcher` — low-level HTTP fetch utilities for RGS endpoints * `rgs-requests` — typed request functions (`requestBet`, `requestEndRound`, etc.) ## Context-providing packages Four packages create [Svelte context](https://svelte.dev/docs/svelte/context) that child components can read with a `getContext*()` call. `setContext*()` must be called at the app entry level (e.g. `+page.svelte`) before any child tries to consume it. | Package | Creates | Context helpers | | --------------------- | ----------------------------------- | --------------------------------------------------- | | `pixi-svelte` | `stateApp` | `setContextApp` / `getContextApp` | | `utils-event-emitter` | `eventEmitter` | `setContextEventEmitter` / `getContextEventEmitter` | | `utils-layout` | `stateLayout`, `stateLayoutDerived` | `setContextLayout` / `getContextLayout` | | `utils-xstate` | `stateXstate`, `stateXstateDerived` | `setContextXstate` / `getContextXstate` | ```ts theme={null} // apps/lines/src/game/context.ts export const setContext = () => { setContextEventEmitter({ eventEmitter }); setContextXstate({ stateXstate, stateXstateDerived }); setContextLayout({ stateLayout, stateLayoutDerived }); setContextApp({ stateApp }); }; ``` # pixi-svelte Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/pixi-svelte Svelte 5 components wrapping PixiJS 8 primitives for declarative game rendering `pixi-svelte` is the rendering layer of the Stake Engine Web SDK. It wraps [PixiJS 8](https://www.npmjs.com/package/pixi.js) display objects as declarative [Svelte 5](https://www.npmjs.com/package/svelte) components, so you can build a 2D game scene the same way you build a web page. It is also published as a standalone npm package at [npmjs.com/package/pixi-svelte](https://www.npmjs.com/package/pixi-svelte). **Current version:** `2.1.0` ## Key dependencies | Dependency | Version | | --------------------------------- | ------- | | `pixi.js` | 8.8.1 | | `@esotericsoftware/spine-pixi-v8` | 4.2.74 | | `@barvynkoa/particle-emitter` | 0.0.1 | | `svelte` | 5.20.5 | | `webfontloader` | 1.6.28 | ## Components All components are exported from the package root: ```ts theme={null} import { App, Container, Sprite, BaseSprite, Text, BitmapText, AnimatedSprite, SpriteSheet, Graphics, Circle, Rectangle, SpineProvider, BaseSpineProvider, SpineTrack, SpineBone, SpineSlot, SpineEventEmitterProvider, ParticleContainer, ParticleEmitter, Particles, } from 'pixi-svelte'; ``` ### Component reference | Component | Description | | --------------------------- | -------------------------------------------------------------------------------- | | `App` | Root PixiJS application container. Set `resizeTo: window` for full-screen games. | | `Container` | Groups child display objects. Maps to `PIXI.Container`. | | `Sprite` | Renders a texture from `loadedAssets`. | | `BaseSprite` | Low-level sprite without asset loading wiring. | | `Text` | Dynamic text using `PIXI.Text`. | | `BitmapText` | Performant text using pre-rendered bitmap fonts (`PIXI.BitmapText`). | | `AnimatedSprite` | Frame-by-frame animation from a texture array. | | `SpriteSheet` | Plays named animations from a spritesheet atlas. | | `Graphics` | Imperative drawing API (`PIXI.Graphics`). | | `Circle` | Convenience wrapper to draw a circle. | | `Rectangle` | Convenience wrapper to draw a rectangle. | | `SpineProvider` | Loads and owns a Spine skeleton; provides spine context to children. | | `BaseSpineProvider` | Low-level Spine provider without asset wiring. | | `SpineTrack` | Sets and plays a Spine animation on a specific track index. | | `SpineBone` | Positions a container at a named Spine bone's world position. | | `SpineSlot` | Renders content at a named Spine slot. | | `SpineEventEmitterProvider` | Bridges Spine animation events to an event emitter. | | `ParticleContainer` | Optimized container for rendering large numbers of particles. | | `ParticleEmitter` | Emits particles using the `@barvynkoa/particle-emitter` config. | | `Particles` | Companion to `ParticleContainer` for particle management. | ## createApp() Creates the reactive `stateApp` object that drives the `` component and tracks asset loading. ```ts theme={null} import { createApp } from 'pixi-svelte'; const { stateApp } = createApp({ assets }); ``` ### stateApp fields ```ts theme={null} const stateApp = $state({ reset, // () => void — resets all fields to defaults assets, // Assets — asset manifest passed to createApp() loaded: false, // boolean — true once all assets have finished loading loadingProgress: 0, // number — 0–100 loading progress value loadedAssets: {} as LoadedAssets, // processed PIXI.Assets keyed by name pixiApplication: undefined as PIXI.Application | undefined, }); ``` `loadedAssets` is populated by `PIXI.Assets.load` and can be passed directly to `` and other pixi-svelte components. ## ContextApp `stateApp` is shared with child components through Svelte context: ```ts theme={null} import { setContextApp, getContextApp } from 'pixi-svelte'; // In entry component (e.g. +page.svelte or a story) setContextApp({ stateApp }); // In any descendant component const { stateApp } = getContextApp(); ``` The context key is `'@@pixi_svelte'`. `pixi-svelte` uses the **built** output in `dist/`. If you change any source file under `packages/pixi-svelte/src/`, rebuild before your changes take effect: ```bash theme={null} pnpm run build --filter=pixi-svelte ``` ## Usage example ```svelte theme={null} {#if stateApp.loaded} {/if} ``` # utils-book Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-book Utilities for processing ordered sequences of bookEvents from the RGS `utils-book` provides the functions that drive the core game loop: receiving a `book` (JSON payload) from the RGS and processing its `events` array one-by-one in order. ## createPlayBookUtils() Factory function that returns `playBookEvent` and `playBookEvents`. You pass it the `bookEventHandlerMap` for your game once at startup. ```ts theme={null} import { createPlayBookUtils } from 'utils-book'; export function createPlayBookUtils< TBookEventHandlerMap extends BookEventHandlerMap >({ bookEventHandlerMap, debug, }: { bookEventHandlerMap: TBookEventHandlerMap; debug?: boolean; }) ``` ### Parameters | Parameter | Type | Description | | --------------------- | -------------------------------------------------- | ------------------------------------------------------------------------ | | `bookEventHandlerMap` | `BookEventHandlerMap` | Map from `bookEvent.type` string to an async handler function. | | `debug` | `boolean?` | When `true`, logs each `bookEvent` to the console before dispatching it. | ### Return value ```ts theme={null} { playBookEvent: (bookEvent: TBookEvent, bookEventContext: TBookEventContext) => Promise; playBookEvents: (bookEvents: TBookEvent[], bookEventContext?: BookEventContextFromMapWithoutBookEvents) => Promise; } ``` ## playBookEvents() Processes an entire `book.events` array, resolving each handler **one after another** using `sequence()` from `utils-shared`. The order of events is critical — it determines the visual sequence of the game (e.g. spin before win reveal). ```ts theme={null} const playBookEvents = async ( bookEvents: TBookEvent[], bookEventContext?: BookEventContextFromMapWithoutBookEvents, ) => { const finalBookEventContext = bookEventContext || ({} as BookEventContextFromMapWithoutBookEvents); await sequence(bookEvents, async (bookEvent) => { await playBookEvent(bookEvent, { ...finalBookEventContext, bookEvents }); }); }; ``` `bookEvents` is automatically injected into the context of every handler call, so handlers can inspect the full event list. `sequence()` from `utils-shared` resolves async functions one-after-another, unlike `Promise.all()` which fires them simultaneously. This is essential for maintaining the correct animation order. ## playBookEvent() Dispatches a single `bookEvent` to the matching handler in `bookEventHandlerMap`. If no handler is found for `bookEvent.type`, an error is logged. ```ts theme={null} const playBookEvent = async ( bookEvent: TBookEvent, bookEventContext: TBookEventContext, ) => { const bookEventHandler = bookEventHandlerMap?.[bookEvent.type]; if (bookEventHandler) { if (debug) console.log(bookEvent); await bookEventHandler(bookEvent, bookEventContext); } else { console.error('Missing bookEventHandler in "bookEventHandlerMap" for: ', bookEvent); } }; ``` This is also used directly by the `MODE_/bookEvent/` Storybook stories to test individual events in isolation. ## Types ```ts theme={null} // A bookEvent must always have these two fields export type BaseBookEvent = { index: number; type: string }; // A handler for a single bookEvent type export type BookEventHandler< TBookEvent extends BaseBookEvent, THandlerContext extends object, > = (bookEvent: TBookEvent, context: THandlerContext) => Promise; // The full map: keys are bookEvent.type strings export type BookEventHandlerMap< TBookEvent extends BaseBookEvent, THandlerContext extends object, > = Record>; // Helper to extract the BookEvent type from a map export type GetBookEventFromMap> = T extends BookEventHandlerMap ? U : never; // Helper to extract the context type from a map export type GetBookEventContextFromMap> = T extends BookEventHandlerMap ? U : never; ``` ## Real-world usage ```ts theme={null} // apps/lines/src/game/bookEventHandlerMap.ts export const bookEventHandlerMap: BookEventHandlerMap = { reveal: async (bookEvent) => { await eventEmitter.broadcastAsync({ type: 'boardSpin', board: bookEvent.board }); }, setTotalWin: async (bookEvent) => { eventEmitter.broadcast({ type: 'totalWinUpdate', amount: bookEvent.amount }); }, updateFreeSpin: async (bookEvent) => { eventEmitter.broadcast({ type: 'freeSpinCounterShow' }); eventEmitter.broadcast({ type: 'freeSpinCounterUpdate', current: bookEvent.amount, total: bookEvent.total, }); }, }; // Create utilities once at game startup export const { playBookEvent, playBookEvents } = createPlayBookUtils({ bookEventHandlerMap, debug: import.meta.env.DEV, }); ``` ## createMultiBookUtils() For games that receive multiple books simultaneously (e.g. multi-way games), `createMultiBookUtils` provides helpers to split, merge, and re-assemble books into rounds. ```ts theme={null} import { createMultiBookUtils } from 'utils-book'; const { booksToRounds, roundsToBooks } = createMultiBookUtils(); ``` ### booksToRounds() Splits multiple books into parallel rounds aligned by a `splitBy` event type. ```ts theme={null} booksToRounds({ books: TBookEvent[][], // array of books (one per game mode) splitBy: TBookEvent['type'][], // event type(s) that delimit round boundaries }): TBookEvent[][][] ``` ### roundsToBooks() Reassembles rounds back into per-book arrays: ```ts theme={null} roundsToBooks({ rounds: TBookEvent[][][], }): TBookEvent[][] ``` # utils-event-emitter Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-event-emitter Type-safe event bus connecting game logic to Svelte components `utils-event-emitter` implements [event-driven programming](https://en.wikipedia.org/wiki/Event-driven_programming) for the SDK. It connects the JavaScript scope (book event handlers, state machines) to Svelte component scope without passing props through deep component trees. ## createEventEmitter() Factory function typed by a union of all possible emitter events. Create it once per game in `src/game/eventEmitter.ts`. ```ts theme={null} import { createEventEmitter } from 'utils-event-emitter'; export function createEventEmitter() ``` ### Constraint `TEmitterEvent` must extend `EmitterEventBase`: ```ts theme={null} export type EmitterEventBase = { type: string; }; ``` In practice every game defines a discriminated union type: ```ts theme={null} export type EmitterEvent = | { type: 'boardSpin'; board: RawBoard } | { type: 'boardSettle'; board: RawBoard } | { type: 'totalWinUpdate'; amount: number } | { type: 'freeSpinCounterShow' } | { type: 'freeSpinCounterHide' } | { type: 'freeSpinCounterUpdate'; current?: number; total?: number }; export const { eventEmitter } = createEventEmitter(); ``` ### Return value ```ts theme={null} { eventEmitter: { subscribeOnMount: (map: Partial) => void; broadcast: (emitterEvent: TEmitterEvent) => void; broadcastAsync: (emitterEvent: TEmitterEvent) => Promise; } } ``` ## Methods ### subscribeOnMount() Registers a map of handlers inside a Svelte component's `onMount` lifecycle. Automatically unsubscribes when the component is destroyed. ```ts theme={null} const subscribeOnMount = ( emitterEventHandlerMap: Partial, ) => void ``` The `EmitterEventHandlerMap` type is keyed by every `type` in the `TEmitterEvent` union, and each value receives the narrowed event type for that key: ```ts theme={null} type EmitterEventHandlerMap = { [T in EmitterEventType]: (emitterEvent: Extract) => any }; ``` **Example — sync handler:** ```svelte theme={null} ``` ### broadcast() Synchronously dispatches an event to all current subscribers. Use this for fire-and-forget updates (show/hide a component, update a counter). ```ts theme={null} const broadcast = (emitterEvent: TEmitterEvent) => void ``` ```ts theme={null} eventEmitter.broadcast({ type: 'freeSpinCounterShow' }); eventEmitter.broadcast({ type: 'freeSpinCounterUpdate', current: 3, total: 10 }); ``` Internally it iterates the subscription `Set` and calls each handler synchronously: ```ts theme={null} const broadcast = (emitterEvent: TEmitterEvent) => { subscriptions.forEach((emitterEventHandler) => { emitterEventHandler(emitterEvent); }); }; ``` ### broadcastAsync() Dispatches an event to all subscribers and returns `Promise.all` of every handler's return value. Use `await eventEmitter.broadcastAsync(...)` when you need to wait for all animations or async operations triggered by the event to finish. ```ts theme={null} const broadcastAsync = (emitterEvent: TEmitterEvent) => Promise ``` ```ts theme={null} // bookEventHandlerMap.ts await eventEmitter.broadcastAsync({ type: 'freeSpinIntroUpdate', totalFreeSpins: bookEvent.totalFs, }); ``` ```svelte theme={null} ``` ## ContextEventEmitter `utils-event-emitter` also exports context helpers so `eventEmitter` can be read by any descendant component without prop drilling: ```ts theme={null} import { setContextEventEmitter, getContextEventEmitter, } from 'utils-event-emitter'; ``` ```ts theme={null} // context.ts (context key: '@@eventEmitter') export function setContextEventEmitter( value: ContextEventEmitter, ): void export function getContextEventEmitter(): ContextEventEmitter ``` `setContextEventEmitter` must be called at the entry level of the app (same place as the other `setContext*` calls). ## Typical setup pattern ```ts theme={null} // apps/lines/src/game/eventEmitter.ts import { createEventEmitter } from 'utils-event-emitter'; import type { EmitterEventUi } from 'components-ui-pixi'; import type { EmitterEventGame } from './typesEmitterEvent'; export type EmitterEvent = EmitterEventUi | EmitterEventGame; export const { eventEmitter } = createEventEmitter(); ``` ```ts theme={null} // apps/lines/src/game/context.ts import { setContextEventEmitter } from 'utils-event-emitter'; import { eventEmitter } from './eventEmitter'; export const setContext = () => { setContextEventEmitter({ eventEmitter }); // ... other contexts }; ``` ```svelte theme={null} ``` # utils-layout Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-layout Responsive canvas layout system with device type detection `utils-layout` solves the challenge of positioning PixiJS display objects across a wide range of screen sizes and orientations. Unlike HTML, PixiJS does not have automatic layout flow — every element must be placed with explicit coordinates. `utils-layout` provides reactive size, ratio, and device-type values derived from the browser window. ## createLayout() Factory function that returns `stateLayout` and `stateLayoutDerived`. Create it once per game. ```ts theme={null} import { createLayout } from 'utils-layout'; export const { stateLayout, stateLayoutDerived } = createLayout({ backgroundRatio: { normal: 1.777, // 16:9 landscape background portrait: 0.5625, // 9:16 portrait background }, mainSizesMap: { desktop: { width: 1920, height: 1080 }, tablet: { width: 1920, height: 1920 }, landscape: { width: 1920, height: 1080 }, portrait: { width: 1080, height: 1920 }, }, }); ``` ### Parameters | Parameter | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `backgroundRatio.normal` | Aspect ratio of the landscape background asset (width / height). | | `backgroundRatio.portrait` | Aspect ratio of the portrait background asset. | | `mainSizesMap` | Design-space dimensions for each layout type — the stage is scaled to fit within the canvas while preserving this aspect ratio. | ## stateLayout Mutable reactive state. Currently contains one field: ```ts theme={null} const stateLayout = $state({ showLoadingScreen: true, }); ``` Set `stateLayout.showLoadingScreen = false` once all assets have loaded to transition the game into the playable state. ## stateLayoutDerived All values are **functions** (not plain values) because they are computed from `innerWidth.current` and `innerHeight.current` from Svelte's `svelte/reactivity/window` — they update automatically whenever the window is resized. ```ts theme={null} const stateLayoutDerived = { canvasSizes, // () => { width: number; height: number } canvasRatio, // () => number (width / height) canvasRatioType, // () => 'longWidth' | 'almostSquare' | 'longHeight' canvasSizeType, // () => 'smallMobile' | 'mobile' | 'tablet' | 'largeTablet' | 'desktop' layoutType, // () => 'desktop' | 'tablet' | 'landscape' | 'portrait' isStacked, // () => boolean (portrait or almostSquare) mainLayout, // () => { x, y, scale, width, height, anchor } mainLayoutStandard, // () => { x, y, scale, width, height, anchor } (uses STANDARD_MAIN_SIZES_MAP) normalBackgroundLayout, // ({ scale }) => { x, y, width? } | { x, y, height? } portraitBackgroundLayout,// ({ scale }) => { x, y, width? } | { x, y, height? } }; ``` ### canvasRatioType break points | Type | Condition | | -------------- | ------------------- | | `longWidth` | ratio ≥ 1.3 | | `almostSquare` | 0.8 \< ratio \< 1.3 | | `longHeight` | ratio ≤ 0.8 | ### canvasSizeType break points (shortest dimension) | Type | Max size | | ------------- | ----------------------- | | `smallMobile` | 375 px (e.g. iPhone SE) | | `mobile` | 480 px (e.g. iPhone XR) | | `tablet` | 820 px (e.g. iPad Air) | | `largeTablet` | 1024 px (e.g. iPad Pro) | | `desktop` | > 1024 px | ### layoutType derivation ```ts theme={null} const layoutType = () => { if (canvasRatioType() === 'almostSquare') return 'tablet'; if (canvasRatioType() === 'longHeight') return 'portrait'; if (canvasSizeType() === 'mobile' || canvasSizeType() === 'smallMobile') return 'landscape'; return 'desktop'; }; ``` ## How canvasSizes is driven Because `PIXI.Application` is configured with `resizeTo: window`, the PixiJS canvas always matches the window. `createLayout` reads `innerWidth` and `innerHeight` from `svelte/reactivity/window` so all derived values stay in sync with every window resize: ```ts theme={null} import { innerWidth, innerHeight } from 'svelte/reactivity/window'; const canvasSizes = () => ({ width: innerWidth.current ?? 1, height: innerHeight.current ?? 1, }); ``` ## Positioning PixiJS elements For HTML, elements flow automatically. In PixiJS, coordinates must be explicit. `stateLayoutDerived.canvasSizes()` provides the canvas boundary: ```svelte theme={null} ``` Coordinates are relative to the parent ``. When a component is a direct child of ``, `canvasSizes()` gives the canvas boundary. Inside a nested ``, positions are relative to that container's origin. ## isStacked `isStacked()` returns `true` for `portrait` and `tablet` (almostSquare) layout types. Use it to switch between stacked (vertical) and side-by-side (horizontal) UI arrangements: ```svelte theme={null} {#if context.stateLayoutDerived.isStacked()} {:else} {/if} ``` ## ContextLayout ```ts theme={null} import { setContextLayout, getContextLayout } from 'utils-layout'; // Entry component setContextLayout({ stateLayout, stateLayoutDerived }); // Any descendant component const { stateLayout, stateLayoutDerived } = getContextLayout(); ``` Context key: `'@@layout'`. # utils-slots Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-slots Slot machine mechanics: reel creation and board spinning utilities `utils-slots` provides the low-level reel and board primitives for slot machine games. It ships two reel types (spinning and cascading), a board enhancement layer, and shared slot state. ## Reel types ### createReelForSpinning() Creates a reel that animates by sliding symbols downward into view — the classic slot machine spin mechanic. ```ts theme={null} import { createReelForSpinning } from 'utils-slots'; const reel = createReelForSpinning(reelOptions); ``` **Options (`SpinningReelCreateOptions`):** ```ts theme={null} type SpinningReelCreateOptions = { initialSymbols: TRawSymbol[]; // starting symbols on this reel initialSymbolState: TSymbolState; // initial render state of each symbol reelIndex: number; // 0-based column index (used for stagger delay) symbolHeight: number; // height in pixels of each symbol slot onReelStopping: () => void; // called when reel begins to decelerate onSymbolLand: (args: { rawSymbol: TRawSymbol }) => void; // called per symbol on landing }; ``` **Return value:** ```ts theme={null} { reelIndex: number; symbolHeight: number; onReelStopping: () => void; reelLength: number; reelState: SpinningReelState; // reactive $state preSpin: (options) => Promise; // begins the pre-spin loop prepareToSpin: (options) => number; // loads target symbols, returns paddingSize spin: () => Promise; // executes the spin animation stop: () => void; // interrupts mid-spin (turbo) setSymbolsWithRawSymbols: (rawSymbols?) => void; // set symbols without animation readyToSpinEffect: () => void; // registers $effect to signal ready } ``` **`reelState` fields:** ```ts theme={null} const reelState = $state({ symbols: ReelSymbol[]; // current symbols in view + padding motion: 'stopped' | 'spinning' | 'bouncing'; spinType: 'normal' | 'fast' | 'anticipated'; anticipating: boolean; // true while waiting for an anticipated reel readyToSpin: () => void; // resolved when reel is positioned at defaultY spinOptions: () => SpinningReelSpinOptions; }); ``` **Spin type options (`SpinningReelSpinOptions`):** ```ts theme={null} type SpinningReelSpinOptions = { reelPreSpinSpeed: number; // px/ms — pre-spin slide speed reelBounceBackSpeed: number; // px/ms — bounce back after stop reelSpinSpeed: number; // px/ms — main spin speed reelSpinSpeedBeforeBounce: number; // px/ms — deceleration before bounce reelBounceSizeMulti: number; // multiplier of symbolHeight for bounce distance reelPaddingMultiplierNormal: number; // extra padding symbols for normal spin reelPaddingMultiplierAnticipated: number; reelSpinDelay: number; // ms — stagger delay between reels }; ``` ### createReelForCascading() Creates a reel that animates symbols falling out downward and new symbols falling in from above — the cascade/tumble mechanic. ```ts theme={null} import { createReelForCascading } from 'utils-slots'; const reel = createReelForCascading(reelOptions); ``` The API surface is identical to `createReelForSpinning` (same `preSpin`, `prepareToSpin`, `spin`, `stop`, `setSymbolsWithRawSymbols`, `readyToSpinEffect` methods). The cascading reel exposes a `CascadingReelMotion` state: `'fallingOut' | 'hanging' | 'fallingIn' | 'stopped'`. **Cascading spin options (`CascadingReelSpinOptions`):** ```ts theme={null} type CascadingReelSpinOptions = { symbolFallInSpeed: number; // px/ms symbolFallInInterval: number; // ms between symbols falling in symbolFallInBounceSpeed: number; symbolFallInBounceSizeMulti: number; symbolFallOutSpeed: number; symbolFallOutInterval: number; reelFallInDelay: number; // ms delay before reel starts falling in reelPaddingMultiplierNormal: number; reelPaddingMultiplierAnticipated: number; reelFallOutDelay: number; }; ``` ## Board enhancement layer The board enhancement layer wraps an array of reels and coordinates spin timing, anticipation, and pre-spin across all columns. ### createEnhanceBoard() Returns an `enhanceBoard` function that adds `preSpin`, `spin`, `settle`, `stop`, and `readyToSpinEffect` to any array of reels: ```ts theme={null} import { createEnhanceBoard } from 'utils-slots'; const { enhanceBoard } = createEnhanceBoard(); const board = enhanceBoard({ board: myReelsArray }); // board: { board, preSpin, spin, settle, stop, readyToSpinEffect } ``` ### preSpin (createEnhanceBoardPreSpin) Starts the pre-spin animation loop on all reels simultaneously. Sets `stateSlots.isPreSpinning = true`: ```ts theme={null} const preSpin = async ({ paddingBoard }: { paddingBoard?: TRawSymbol[][] }) => void; ``` The pre-spin loop continues until `spin()` is called. Turbo mode is captured at pre-spin start so it applies uniformly to all reels. ### spin (createEnhanceBoardSpin) Stops the pre-spin loop and drives all reels to their target symbols. Handles anticipation and turbo automatically: ```ts theme={null} async function spin({ revealEvent, paddingBoard, }: { revealEvent: RevealEvent; // must include board, anticipation, paddingPositions paddingBoard?: TRawSymbol[][]; }): Promise ``` Anticipation is driven by `revealEvent.anticipation` — an array of numbers, one per reel. Reels after the first anticipated reel set `noStop = true` so they keep spinning until explicitly released. ### settle Resets all reels to the provided raw board without animation (used after bonus entry, tumble settle, etc.): ```ts theme={null} const settle = (rawBoard?: TRawSymbol[][]) => void; ``` ## stateSlots Shared slot state readable from any component: ```ts theme={null} import { stateSlots } from 'utils-slots'; // stateSlots.isPreSpinning — true while reels are in the pre-spin loop ``` ## createGetEmptyPaddedBoard() Utility to create an empty board grid (with 2 padding rows per reel for cascading reels): ```ts theme={null} import { createGetEmptyPaddedBoard } from 'utils-slots'; const { getEmptyBoard } = createGetEmptyPaddedBoard({ reelsDimensions: { x: 5, y: 3 }, // 5 reels, 3 visible symbols each }); const emptyBoard = getEmptyBoard(); // Returns: TRawSymbol[][] with null values, shape [5][5] (3 + 2 padding rows) ``` ## Type reference ```ts theme={null} export type SpinType = 'normal' | 'fast' | 'anticipated'; export type SpinningReel = ReturnType< typeof createReelForSpinning >; export type CascadingReel = ReturnType< typeof createReelForCascading >; export type Reel = | SpinningReel | CascadingReel; // Extract TRawSymbol from a reel type export type GetRawSymbolFromReel> = NonNullable< FirstArgOf >[number]; ``` # utils-sound Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-sound Audio management using Howler.js for music and sound effects `utils-sound` wraps [Howler.js](https://www.npmjs.com/package/howler) to provide a structured audio system with three playback modes: looping music, looping sound effects, and one-shot sound effects. Volume levels are automatically synced to the global `stateSound` from `state-shared`. ## createSound() Factory function typed by your game's sound name union. Create one instance per game. ```ts theme={null} import { createSound } from 'utils-sound'; type SoundName = 'bgMusic' | 'spinReel' | 'winCoin' | 'buttonClick'; const sound = createSound(); ``` ### Return value ```ts theme={null} { load: (loadedAudio: LoadedAudio) => { destroy: () => void }; stop: (stopOptions: StopOptions) => void; fade: (fadeOptions: FadeOptions) => Promise; rate: (rateOptions: RateOptions) => void; volumeEffect: () => void; // register $effect to sync volume from stateSound enableEffect: () => void; // register $effect to mute/unmute on tab visibility get players(): { music: Player; loop: Player; once: Player }; } ``` ## load() Must be called once after assets have loaded. Receives a `LoadedAudio` object from `stateApp.loadedAssets` (populated by `pixi-svelte`). ```ts theme={null} const { destroy } = sound.load(stateApp.loadedAssets['sounds']); ``` Internally creates a single `Howl` instance and three `Player` instances: | Player | Loop | Behaviour | | --------------- | ---- | -------------------------------------------------------------------------------------- | | `players.music` | Yes | Only one music track plays at a time. Starting a new track pauses the current one. | | `players.loop` | Yes | Starts a looping sound effect; does nothing if already playing. | | `players.once` | No | Plays a one-shot sound; auto-removes from map on end. Supports `forcePlay` to restart. | ## Player methods Each player in `sound.players` exposes: ```ts theme={null} { play: (options: PlayOptions & { forcePlay?: boolean }) => void; stop: (options: StopOptions) => void; fade: (options: FadeOptions) => Promise; rate: (options: RateOptions) => void; volume:(volume: number) => void; howl: Howl; debug: () => void; } ``` ## Playback examples ```ts theme={null} // Play background music (resumes if paused, no-op if already playing) sound.players.music.play({ name: 'bgMusic' }); // Play a looping sound effect sound.players.loop.play({ name: 'reelSpin' }); // Stop the looping sound effect sound.stop({ name: 'reelSpin' }); // Play a one-shot sound sound.players.once.play({ name: 'winCoin' }); // Force restart a one-shot (even if currently playing) sound.players.once.play({ name: 'buttonClick', forcePlay: true }); // Fade out music over 1 second await sound.fade({ name: 'bgMusic', from: 1, to: 0, duration: 1000 }); // Change playback rate (e.g. turbo mode) sound.rate({ name: 'reelSpin', rate: 1.5 }); ``` ## Type reference ```ts theme={null} export type PlayOptions = { name: TSoundName }; export type StopOptions = { name: TSoundName }; export type FadeOptions = { name: TSoundName; from: number; to: number; duration: number }; export type RateOptions = { name: TSoundName; rate: number }; export type SoundState = 'new' | 'playing' | 'paused'; export type SoundConfig = { volume: number }; export type GetSound = { soundId: number; soundName: TSoundName; soundState: SoundState; soundConfig: SoundConfig; // per-sound volume multiplier from the asset config soundVolume: number; // current fade volume (0–1) }; ``` ## Volume system Volume is controlled globally via `stateSound` from `state-shared`. All three players observe this state via Svelte `$effect`. ```ts theme={null} // state-shared/src/stateSound.svelte.ts const DEFAULT_VOLUME_VALUE = 75; // 0–100 scale export const stateSound = $state({ volumeValueMaster: DEFAULT_VOLUME_VALUE, volumeValueMusic: DEFAULT_VOLUME_VALUE, volumeValueSoundEffect: DEFAULT_VOLUME_VALUE, }); export const stateSoundDerived = { volumeMaster: () => stateSound.volumeValueMaster / 100, volumeMusic: () => (stateSound.volumeValueMusic / 100) * stateSoundDerived.volumeMaster(), volumeSoundEffect: () => (stateSound.volumeValueSoundEffect / 100) * stateSoundDerived.volumeMaster(), }; ``` Call `sound.volumeEffect()` inside a Svelte component (e.g. the root `Game.svelte`) to register the reactive effects that keep player volume in sync: ```svelte theme={null} ``` ## Enable / disable `enableEffect()` registers a `$effect` that calls `Howler.mute(true)` whenever the tab is hidden or the `AudioContext` is not running (e.g. the browser blocked autoplay), and `Howler.mute(false)` when both conditions clear: ```ts theme={null} const enableEffect = () => { $effect(() => { if (audioContextState === 'running' && visibilityState === 'visible') { Howler.volume(1); Howler.mute(false); } else { Howler.volume(0); Howler.mute(true); } }); }; ``` The UI components in `components-ui-pixi` expose a `ButtonSoundSwitch` component that lets the player toggle sound on/off by writing to `stateSound.volumeValueMaster`. ## Full setup example ```ts theme={null} // apps/lines/src/game/sound.ts import { createSound } from 'utils-sound'; export type SoundName = | 'bgMusic' | 'bonusMusic' | 'spinReel' | 'stopReel' | 'winSmall' | 'winBig'; export const sound = createSound(); ``` ```svelte theme={null} ``` # utils-xstate Source: https://www.mintlify.com/StakeEngine/web-sdk/packages/utils-xstate XState finite state machine for managing game bet flows `utils-xstate` wraps [XState v5](https://stately.ai/docs/) to provide a robust [finite state machine](https://en.wikipedia.org/wiki/Finite-state_machine) for the betting lifecycle. It eliminates deeply nested `if-else` logic and provides a clean API for UI components to react to game state. ## createXstate() Creates the reactive `stateXstate` and `stateXstateDerived` objects. Call this once per game. ```ts theme={null} import { createXstate } from 'utils-xstate'; const { stateXstate, stateXstateDerived } = createXstate(); ``` ### stateXstate ```ts theme={null} const stateXstate = $state({ value: '' as StateValue, // current XState machine state name }); ``` ### stateXstateDerived A collection of helper functions that each call `matchesState` from XState internally: ```ts theme={null} const stateXstateDerived = { matchesXstate: (state: string) => boolean, isRendering: () => boolean, // machine is in 'rendering' state isIdle: () => boolean, // machine is in 'idle' state isBetting: () => boolean, // machine is in 'bet' state isAutoBetting: () => boolean, // machine is in 'autoBet' state isResumingBet: () => boolean, // machine is in 'resumeBet' state isPlaying: () => boolean, // NOT rendering AND NOT idle }; ``` `isPlaying()` is the most commonly used helper — it returns `true` whenever any bet flow is active. ## State machine states The `gameActor` machine has five states: | State | Constant | Description | | ----------- | ------------------ | -------------------------------------------------------- | | `rendering` | `STATE_RENDERING` | Initial state; waiting for the loading screen to finish. | | `idle` | `STATE_IDLE` | Ready to accept a bet. | | `bet` | `STATE_BET` | A single bet is in progress. | | `autoBet` | `STATE_AUTOBET` | Auto-spin is running with a countdown. | | `resumeBet` | `STATE_RESUME_BET` | Resuming an unfinished bet from a previous session. | ```ts theme={null} // constants.ts export const STATE_RENDERING = 'rendering' as const; export const STATE_IDLE = 'idle' as const; export const STATE_BET = 'bet' as const; export const STATE_AUTOBET = 'autoBet' as const; export const STATE_RESUME_BET = 'resumeBet' as const; ``` ## createGameActor() Builds the XState machine and actor using `setup()` + `createActor()` from XState: ```ts theme={null} import { setup, createActor } from 'xstate'; const gameMachine = setup({ actors: { bet: intermediateMachines.bet, autoBet: intermediateMachines.autoBet, resumeBet: intermediateMachines.resumeBet, }, }).createMachine({ initial: 'rendering', states: { rendering: { on: { RENDERED: { target: 'idle' } } }, idle: { on: { BET: { target: 'bet' }, AUTO_BET: { target: 'autoBet' }, RESUME_BET: { target: 'resumeBet' }, }, }, bet: { invoke: { src: 'bet', onDone: 'idle' } }, autoBet: { invoke: { src: 'autoBet', onDone: 'idle' } }, resumeBet: { invoke: { src: 'resumeBet', onDone: 'idle' } }, }, }); const gameActor = createActor(gameMachine); ``` ## createIntermediateMachines() Creates the three sub-machines (`bet`, `autoBet`, `resumeBet`) that are invoked by the game actor. Each is built from **primary machine** actors (`newGame`, `playGame`, `endGame`, `resumeGame`) provided by your game. ```ts theme={null} import { createIntermediateMachines } from 'utils-xstate'; const intermediateMachines = createIntermediateMachines({ resumeGame, newGame, playGame, endGame, }); // Returns: { bet, autoBet, resumeBet } ``` ## createPrimaryMachines() Creates the four XState `fromPromise` actors that form the inner bet loop. You provide callbacks that connect the machine to your game's RGS requests and animation layer. ```ts theme={null} function createPrimaryMachines(options: { onResumeGameActive: (betToResume: TBet) => TBet; onResumeGameInactive: (betToResume: TBet) => void; onNewGameStart: () => Promise | undefined; onNewGameError: () => any; onPlayGame: (bet: TBet) => Promise; checkIsBonusGame: (bet: TBet) => boolean; }) // Returns: { newGame, playGame, endGame, resumeGame } ``` ### BET\_TYPE\_METHODS\_MAP Internally, `createPrimaryMachines` classifies each bet into one of three types and calls `end-round` at the appropriate time: ```ts theme={null} const BET_TYPE_METHODS_MAP = { noWin: { // No RGS end-round calls needed newGame: async () => undefined, endGame: async () => undefined, }, singleRoundWin: { // Call end-round BEFORE playing animations (balance pre-fetched) newGame: async () => { const endRoundData = await handleRequestEndRound(); if (endRoundData?.balance) { balanceAmountFromApiHolder = endRoundData.balance.amount; } }, endGame: async () => { if (balanceAmountFromApiHolder !== null) { handleUpdateBalance({ balanceAmountFromApi: balanceAmountFromApiHolder }); balanceAmountFromApiHolder = null; } }, }, bonusWin: { // Call end-round AFTER all animations finish newGame: async () => undefined, endGame: async () => { const data = await handleRequestEndRound(); if (data?.balance) { handleUpdateBalance({ balanceAmountFromApi: data.balance.amount }); balanceAmountFromApiHolder = null; } }, }, } as const; ``` The timing of `end-round` differs by bet type. Whether a bet can be resumed from the `authenticate` request depends on when `end-round` is called. `bonusWin` bets can be resumed because `end-round` is deferred until after animations. ## ContextXstate ```ts theme={null} import { setContextXstate, getContextXstate } from 'utils-xstate'; // Entry component setContextXstate({ stateXstate, stateXstateDerived }); // Any descendant component const { stateXstate, stateXstateDerived } = getContextXstate(); ``` Context key: `'@@xstate'`. ## UI usage example The most common usage is disabling buttons while the game is playing: ```svelte theme={null} ``` Because `stateXstateDerived` functions are backed by Svelte `$state`, any component that reads them is automatically reactive — the button re-enables as soon as the machine transitions back to `idle`. # Quickstart Source: https://www.mintlify.com/StakeEngine/web-sdk/quickstart Get the Stake Engine Web SDK running locally in minutes This guide walks you through cloning the repo, installing dependencies, and running the `lines` sample game inside Storybook so you can play a full round locally before writing a single line of game code. The SDK requires Node.js version 22.16.0. The recommended way is via [nvm](https://github.com/nvm-sh/nvm): ```bash theme={null} # Download and install nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash # Source nvm without restarting the shell \. "$HOME/.nvm/nvm.sh" # Install Node.js 22.16.0 nvm install 22.16.0 # Verify — should print "v22.16.0" node -v ``` The monorepo uses pnpm workspaces. Install the required version globally: ```bash theme={null} npm install pnpm@10.5.0 -g # Verify — should print "10.5.0" pnpm -v ``` ```bash theme={null} git clone https://github.com/StakeEngine/web-sdk.git cd web-sdk ``` From the repo root, install all workspace dependencies in one command: ```bash theme={null} pnpm install ``` If no error messages appear, you are ready to continue. **Windows users:** Storybook may fail to start without `cross-env`. If you encounter an error, update the storybook script in `apps/lines/package.json`: ```json theme={null} "storybook": "cross-env PUBLIC_CHROMATIC=true storybook dev -p 6001 public" ``` Initial Storybook loading on Windows can take up to 15 minutes, but hot reloads after that are fast. TurboRepo's `--filter` flag targets a specific workspace package by the `name` field in its `package.json`. The sample game is named `lines`: ```bash theme={null} pnpm run storybook --filter=lines ``` Storybook opens in your browser. You will see a sidebar with story sets like `COMPONENTS`, `MODE_BASE`, and `MODE_BONUS`. 1. In the Storybook sidebar, navigate to **MODE\_BASE → book → random**. 2. An **Action** button appears in the bottom-left corner of the game canvas. 3. Click **Action** and wait for a complete base game round to finish. You now have a fully functional game running locally against randomly generated book data—no RGS connection required. ## Next steps Now that the game is running locally, you can explore the rest of the development workflow: * **[Building and Launching](/building-and-launching)** — build for production and deploy to Stake Engine. * **[Book and Events](/concepts/book-and-events)** — understand how bookEvents and emitterEvents drive game behaviour. To run the game in **DEV mode** (a full SvelteKit dev server instead of Storybook), use: ```bash theme={null} pnpm run dev --filter=lines ``` You will see an authentication error screen—this is expected until you connect to a live RGS session. See [Building and Launching](/building-and-launching) for instructions on copying the RGS query string into the DEV mode URL.