Skip to content

Rollerz Framework

The rollerz-framework package is a browser UI toolkit for Rollerz games. It provides ready-made components for bet selection, currency display, game menus, overlays, video playback, and a developer panel for testing.

Installation

Add rollerz-framework as a dependency in your game project:

bash
npm install rollerz-framework

Then import the parts you need:

javascript
import {
  createBetSelector,
  formatAmount,
  formatMultiplier,
  showGameMenu,
  showOverlay,
  playVideo,
  createDevPanel,
} from 'rollerz-framework';

Or import everything under a namespace:

javascript
import * as framework from 'rollerz-framework';

Bet Selector

The bet selector is a composable UI component with three parts: a bet-type selector (segmented buttons), a bet amount picker, and a commit button.

Basic usage

javascript
// Predefined providers expose their bet types directly:
const betSelector = createBetSelector({
  container: document.getElementById('betSelector'),
  currency: session.currency,
  betTypes: sdk.go3.getBetTypes(),           // ['BASE', 'BOOSTED']
  defaultBetType: sdk.go3.getDefaultBetType(), // 'BASE'
  getValidBets: (betType) => sdk.go3.getValidBets({ betType }),
  actionLabel: 'Play!',
  onCommit: ({ amount, betType }) => {
    playRound(amount, betType);
  },
  defaultBet: session.defaultBet,
});

For Stepper, the same call site renders five difficulty buttons (BEGINNERINSANE). For GP, supply your own bet type list — either by calling sdk.gp.configureBetTypes([...]) first and reading sdk.gp.getBetTypes(), or by passing the array directly to createBetSelector.

Options

OptionTypeRequiredDescription
containerHTMLElementYesDOM element to mount the bet selector into
currencyCurrencyYesCurrency object from the game session (prefix, suffix, decimal, precision, grouping)
betTypesreadonly string[]YesThe full ordered list of bet types to render as segmented buttons. Must be non-empty
defaultBetTypestringNoThe bet type selected on mount. Defaults to betTypes[0]
getValidBets(betType: string) => number[] | Promise<number[]>YesReturns the valid bet amounts for the given bet type. Called on mount and whenever the bet type changes
actionLabelstringYesLabel for the commit button (e.g. "Smash!", "Play!", "Bet")
onCommit(commit: { amount, betType }) => voidYesFired when the player clicks the commit button
defaultBetnumberNoPre-select this amount on load (typically session.defaultBet)
onBetTypeChange(betType: string) => voidNoSide-effect callback when the bet type changes (e.g. tint the screen)
commitViewICommitViewNoCustom commit button component (replaces the default)
betPickerIBetPickerNoCustom bet picker component (replaces the default)
betTypeSelectorIBetTypeSelectorNoCustom bet type selector component (replaces the default)

When betTypes has only one entry, the bet type selector hides itself — single-option providers (like an unconfigured GP using a one-element list) render as just a picker + commit button.

Handle methods

createBetSelector returns a BetSelectorHandle with these methods:

MethodDescription
setDisabled(disabled)Disable or enable all sub-components (use during API calls)
setBetType(betType)Programmatically change the active bet type and reload bets
getState()Returns `{ amount: number
destroy()Unmount all sub-components and clear the container

Custom sub-components

Each part of the bet selector can be replaced with a custom implementation. Implement the corresponding interface and pass it in the options:

ICommitView — the commit button and amount display:

typescript
interface ICommitView {
  mount(container: HTMLElement): void;
  unmount(): void;
  setAmount(amount: number | null, currency: Currency): void;
  setLabel(label: string): void;
  setDisabled(disabled: boolean): void;
  onCommit: () => void;
}

IBetPicker — the bet amount selector:

typescript
interface IBetPicker {
  mount(container: HTMLElement): void;
  unmount(): void;
  setOptions(amounts: number[], currency: Currency): void;
  setSelected(amount: number | null): void;
  setDisabled(disabled: boolean): void;
  onChange: (amount: number) => void;
}

IBetTypeSelector — the segmented bet-type selector:

typescript
interface IBetTypeSelector {
  mount(container: HTMLElement): void;
  unmount(): void;
  setOptions(betTypes: readonly string[]): void;
  setValue(betType: string): void;
  getValue(): string;
  setDisabled(disabled: boolean): void;
  onChange: (betType: string) => void;
}

The framework also exports the default implementations (DefaultCommitView, DefaultBetPicker, DefaultBetTypeSelector) so you can extend or compose them.

Currency formatting

formatAmount(rawAmount, currency)

Formats a raw amount (in the smallest currency unit, e.g. cents) into a display string using the currency's formatting rules.

javascript
import { formatAmount } from 'rollerz-framework';

formatAmount(1500, session.currency);
// → "$15.00" (with USD currency)
ParameterTypeExampleDescription
rawAmountnumber1500Amount in the smallest unit (e.g. cents)
currencyCurrencysession.currencyCurrency object with prefix, suffix, decimal, precision, grouping

The function divides by 100 to convert from the smallest unit, applies the precision, adds thousands grouping if configured, and wraps with prefix/suffix.

Why raw amounts?

All SDK providers return monetary values exactly as the math server sends them — in the smallest currency unit (cents). A balance of 10000 means $100.00, a bet of 50 means $0.50. The SDK never scales or converts these values.

This keeps the SDK a faithful passthrough and avoids floating-point precision issues. Use formatAmount() whenever you need to display a monetary value to the player — it handles the conversion and formatting automatically using the currency object from openGame().

formatMultiplier(value, precision?)

Formats a numeric multiplier for display.

javascript
import { formatMultiplier } from 'rollerz-framework';

formatMultiplier(2.5);    // → "2.50×"
formatMultiplier(3, 0);   // → "3×"
ParameterTypeExampleDefaultDescription
valuenumber2.5Multiplier value
precisionnumber02Decimal places

Game Menu

showGameMenu adds a menu bar with a hamburger menu and a settings button to your game. It includes built-in help and sound settings overlays.

javascript
const menu = showGameMenu({
  container: document.getElementById('menuContainer'),
  helpContent: '<p><strong>My Game</strong></p><p>Game instructions here.</p>',
});

// Later: menu.destroy() to remove

Options

OptionTypeRequiredDescription
containerHTMLElementNoContainer to mount the menu bar into. If omitted, mounts to document.body with position: fixed
helpContentstringNoHTML string shown in the help overlay
helpUrlstringNoURL loaded in an iframe inside the help overlay (alternative to helpContent)
onSoundSettings() => voidNoCustom callback for sound settings. If omitted, shows a placeholder overlay

The hamburger menu contains "NEED HELP?" and "SETTINGS" items. The settings button (gear icon) provides quick access to the same features.

You can also use the lower-level functions directly:

  • createHamburgerMenu({ parent, items, onSelect }) — dropdown menu
  • createSettingsButton({ parent, onHelp, onSoundSettings }) — gear button with dropdown

Overlay

showOverlay displays a modal overlay with a title, content, and close button. It can be closed by clicking the X button, clicking the backdrop, or pressing Escape.

javascript
import { showOverlay } from 'rollerz-framework';

showOverlay({
  title: 'Game Rules',
  content: '<p>Place your bet and smash the rocks!</p>',
  onClose: () => {
    console.log('Overlay closed');
  },
});

Options

OptionTypeRequiredDescription
titlestringNoOverlay title text
contentstring | HTMLElementYesHTML string or DOM element to display in the overlay body
onClose() => voidYesCalled when the overlay is closed (by any method)

Video Player

playVideo shows a fullscreen video overlay. The returned promise resolves when the video ends or the user clicks to dismiss.

javascript
import { playVideo } from 'rollerz-framework';

await playVideo({
  url: '/assets/intro.mp4',
  muted: false,
});
// Video finished — continue with game

Options

OptionTypeDefaultDescription
urlstringVideo source URL
parentHTMLElementdocument.bodyContainer to attach the video overlay to
mutedbooleanfalseStart muted
loopbooleanfalseLoop the video (click to dismiss still works)
holdLastFramebooleanfalseKeep the last frame visible after the video ends (instead of removing the overlay)

If the video fails to load, the promise rejects with an error.

Dev Mode

The dev panel is an in-browser floating panel that lets developers toggle "dev mode" on individual providers. When dev mode is enabled for a provider, placeBet bypasses the real RGS entirely and generates mock results locally based on the control values you set in the panel. This is invaluable for testing animations, edge cases, and UI states without making network calls.

Adding the dev panel

javascript
import { createDevPanel } from 'rollerz-framework';

const panel = createDevPanel({
  providers: [
    { name: 'GO3', provider: sdk.go3 },
    { name: 'Stepper', provider: sdk.stepper },
  ],
  position: 'top-right', // 'top-left', 'bottom-right', 'bottom-left'
});

// Later: panel.destroy() to remove

Each entry in providers needs:

  • name — display label in the panel
  • provider — a provider instance (e.g. sdk.go3) that exposes devConfig, setDevEnabled, setDevState, getDevState, isDevEnabled

Panel features

  • Collapsible — click the "−" button to collapse/expand the panel body
  • Draggable — drag the header to reposition the panel anywhere on screen
  • Per-provider toggle — each provider has its own enable/disable switch
  • Position — initial corner: top-right (default), top-left, bottom-right, bottom-left

How it works

When the dev toggle is enabled for a provider:

  1. provider.setDevEnabled(true) is called, which initializes the dev state to devConfig.defaultState
  2. Control changes call provider.setDevState({ key: value }) to update individual state fields
  3. When placeBet is called on that provider, it skips the API call and instead calls devConfig.generateBetResponse(betAmount, betType, devState) to produce a local result
  4. collect also runs locally, updating the session balance from the dev result

DevProviderConfig

Providers define their dev behavior via devConfig:

typescript
interface DevProviderConfig<TDevState, TBetResult> {
  controls: DevControl[];
  defaultState: TDevState;
  generateBetResponse(
    betAmount: number,
    betType: string,
    devState: TDevState,
  ): TBetResult;
}

Control types

The controls array defines what appears in the dev panel for each provider:

TypeDescriptionExample
radioSingle-select radio buttonsCount: 1, 2, 3
sliderNumeric slider with min/max/stepMultiplier: 1–5
checkboxBoolean toggleForce bonus: on/off
textFree-text inputCustom round ID
multiselectMulti-select checkboxesOutcomes: forceLoss, forceWin, forceBonus
groupedCountersRepeatable rows of per-key counters that must sum to a fixed slot countCard packs: C/U/R/L counts summing to 5

Use the devControl helper to create controls:

javascript
import { devControl } from '@rollerz/types';

const controls = [
  devControl.multiselect('outcome', 'Outcome', ['forceLoss', 'forceWin', 'forceBonus']),
  devControl.slider('multiplier', 'Multiplier', 1, 5, 1),
  devControl.radio('count', 'Count', ['1', '2', '3']),
];

groupedCounters takes an options object instead of a flat signature — each row holds one counter per counters[i].key, all counters in a row must sum to slotsPerGroup, and the panel enforces minGroups/maxGroups on the number of rows:

javascript
devControl.groupedCounters('packs', 'Packs', {
  counters: [
    { key: 'COMMON',    label: 'C' },
    { key: 'UNCOMMON',  label: 'U' },
    { key: 'RARE',      label: 'R' },
    { key: 'LEGENDARY', label: 'L' },
  ],
  slotsPerGroup: 5,
  minGroups: 1,
  maxGroups: 11,
  groupLabel: 'Pack',
  addLabel: 'Add Pack',
});

The corresponding dev state is an array of rows keyed by counters[i].key, e.g. [{ COMMON: 2, UNCOMMON: 0, RARE: 3, LEGENDARY: 0 }].

Example: GO3 devConfig

The GO3 provider defines its dev config like this:

typescript
override readonly devConfig: DevProviderConfig<Record<string, unknown>, Go3BetResult> = {
  controls: [
    devControl.multiselect('outcome', 'Outcome', ['forceLoss', 'forceWin', 'forceBonus']),
    devControl.slider('multiplier', 'Multiplier', 1, 5, 1),
    devControl.radio('count', 'Count', ['1', '2', '3']),
  ],
  defaultState: { outcome: [], multiplier: 1, count: 1 },
  generateBetResponse: (betAmount, _betType, devState) => {
    const isLoss = devState.outcome.includes('forceLoss');
    const hasBonus = devState.outcome.includes('forceBonus');
    const multiplier = isLoss ? 0 : devState.multiplier;
    const totalWinAmount = isLoss ? 0 : betAmount * multiplier;
    return {
      roundId: uuidv4(),
      balance: session.balance - betAmount + totalWinAmount,
      totalBetAmount: betAmount,
      totalWinAmount,
      count: isLoss ? 0 : devState.count,
      multiplier,
      hasBonus,
      nextAction: ['collect'],
    };
  },
};

With this config, the dev panel shows:

  • A multiselect for forcing loss, win, or bonus outcomes
  • A slider to control the win multiplier
  • Radio buttons to set how many items get hit

Provider dev configs

GO3, Stepper, and GP ship with their own devConfig, so a single createDevPanel call surfaces controls for each in one panel. Rippin Rumble currently has no dev config — toggling it on in the panel shows "No dev config registered".

ProviderControlsNotes
GO3outcome (forceLoss/forceWin/forceBonus), multiplier, countSingle-shot instant reveal — the result is returned directly from placeBet.
Stepperoutcome (survive/crashAtStep), crashAtStep, payoutGrowthPercent, totalStepsplaceBet returns a pre-computed ladder; subsequent step() calls walk it locally. Player cashes out via the normal collect button — the dev panel has no auto cash-out control. See Stepper dev mode below for the extension pattern.
GPresultJsonSingle free-form JSON patch merged onto the base bet result. GP is a pass-through, so there's no domain control to wire — inject whatever totalWinAmount / math shape your game's parser expects (e.g. {"totalWinAmount": 500, "math": {"details": {"multiplier": 3}}}). balance is recomputed from totalWinAmount so you don't have to keep them in sync. Invalid JSON falls back to an empty patch with a warning.

Stepper dev mode

Stepper is the only built-in provider whose round spans multiple calls (placeBet → step → step → ... → collect). BaseProvider.placeBet() is the only method that consults devConfig.generateBetResponse() by default, so Stepper's step() would otherwise hit the real RGS even in dev mode.

StepperProvider solves this by pre-computing the entire step ladder at placeBet time (inside generateBetResponse) and overriding step() / collect() to walk that ladder locally:

  • generateBetResponse returns a StepperBetResult with steps: [...] precomputed from the bet amount using betAmount * (1 + step * payoutGrowthPercent / 100). currentStep starts at 0 and roundEnded: false. difficulty echoes the player's betType (no override).
  • step() checks isDevEnabled() and walks the stored _devRound, updating currentStep, currentPayout, and each step's outcome ('SAFE' or 'CRASH') based on the outcome control. The round's balance field is kept in sync with the current cash-out value so a manual collect mid-round applies the latest payout.
  • collect() applies _devRound.balance to the session (whether the round has formally ended or not) and clears the local round — this is the player pressing the collect button, whether that's a true cash-out mid-ladder, a crash forfeit, or an auto cash-out at the top.

If you're building a new provider with a multi-call round loop, follow the same pattern: store the pre-computed round in a private field during placeBet, override the in-loop methods with isDevEnabled() fast paths, and override collect() to finalize the balance. See packages/sdk/src/providers/stepper.ts for the reference implementation.

This lets you test every possible game state — specific win amounts, edge cases, gem animations — without making any network calls.

Example: RippinRumble devConfig

RippinRumble uses a single groupedCounters control to let you build the exact pack composition the mock response should return. Each row is one card pack, and the four counters (C/U/R/L) must sum to the fixed pack size of 5:

typescript
override readonly devConfig: DevProviderConfig<Record<string, unknown>, RippinRumbleBetResult> = {
  controls: [
    devControl.groupedCounters('packs', 'Packs', {
      counters: [
        { key: 'COMMON',    label: 'C' },
        { key: 'UNCOMMON',  label: 'U' },
        { key: 'RARE',      label: 'R' },
        { key: 'LEGENDARY', label: 'L' },
      ],
      slotsPerGroup: PACK_SIZE, // 5
      minGroups: 1,
      maxGroups: 11,
      groupLabel: 'Pack',
      addLabel: 'Add Pack',
    }),
  ],
  defaultState: {
    packs: [{ COMMON: 2, UNCOMMON: 0, RARE: 3, LEGENDARY: 0 }],
  },
  generateBetResponse: (betAmount, _betType, devState) => {
    const cardPacks = devState.packs.map((composition) =>
      buildPackFromComposition(betAmount, composition),
    );
    const totalWinAmount = cardPacks.reduce((sum, p) => sum + p.totalPayout, 0);
    return {
      roundId: uuidv4(),
      balance: session.balance - betAmount + totalWinAmount,
      totalBetAmount: betAmount,
      totalWinAmount,
      nextAction: ['collect'],
      cardPacks,
      storage: {},
    };
  },
};

The panel renders an "Add Pack" button (capped at maxGroups), one row per pack with a remove button (blocked below minGroups), and a running sum/slotsPerGroup indicator that flags any row whose counters don't total slotsPerGroup. That makes it trivial to dial in specific rarity distributions — e.g. two packs with all-legendary hits — without touching the math server.

Provider dev config

Each provider ships with a devConfig that surfaces a single, purpose-built control in the dev panel. The following is a summary of the dev configs for each provider:

ProviderControlsNotes
Rippin Rumblepacks (groupedCounters — one row per card pack, counters COMMON/UNCOMMON/RARE/LEGENDARY labeled C/U/R/L)Each row must sum to the fixed PACK_SIZE of 5; row count is bounded by minGroups: 1 / maxGroups: 11. generateBetResponse maps each row through buildPackFromComposition(betAmount, composition) to build the cardPacks array and totals totalWinAmount from pack.totalPayout. nextAction is ['collect'], so the player cashes out via the normal collect flow.

TIP

Remove or conditionally hide the dev panel before shipping to production. Wrap the createDevPanel call in a check like if (import.meta.env.DEV) or use your build tool's dead-code elimination.