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:
npm install rollerz-frameworkThen import the parts you need:
import {
createBetSelector,
formatAmount,
formatMultiplier,
showGameMenu,
showOverlay,
playVideo,
createDevPanel,
} from 'rollerz-framework';Or import everything under a namespace:
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
// 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 (BEGINNER → INSANE). 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
| Option | Type | Required | Description |
|---|---|---|---|
container | HTMLElement | Yes | DOM element to mount the bet selector into |
currency | Currency | Yes | Currency object from the game session (prefix, suffix, decimal, precision, grouping) |
betTypes | readonly string[] | Yes | The full ordered list of bet types to render as segmented buttons. Must be non-empty |
defaultBetType | string | No | The bet type selected on mount. Defaults to betTypes[0] |
getValidBets | (betType: string) => number[] | Promise<number[]> | Yes | Returns the valid bet amounts for the given bet type. Called on mount and whenever the bet type changes |
actionLabel | string | Yes | Label for the commit button (e.g. "Smash!", "Play!", "Bet") |
onCommit | (commit: { amount, betType }) => void | Yes | Fired when the player clicks the commit button |
defaultBet | number | No | Pre-select this amount on load (typically session.defaultBet) |
onBetTypeChange | (betType: string) => void | No | Side-effect callback when the bet type changes (e.g. tint the screen) |
commitView | ICommitView | No | Custom commit button component (replaces the default) |
betPicker | IBetPicker | No | Custom bet picker component (replaces the default) |
betTypeSelector | IBetTypeSelector | No | Custom 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:
| Method | Description |
|---|---|
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:
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:
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:
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.
import { formatAmount } from 'rollerz-framework';
formatAmount(1500, session.currency);
// → "$15.00" (with USD currency)| Parameter | Type | Example | Description |
|---|---|---|---|
rawAmount | number | 1500 | Amount in the smallest unit (e.g. cents) |
currency | Currency | session.currency | Currency 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.
import { formatMultiplier } from 'rollerz-framework';
formatMultiplier(2.5); // → "2.50×"
formatMultiplier(3, 0); // → "3×"| Parameter | Type | Example | Default | Description |
|---|---|---|---|---|
value | number | 2.5 | — | Multiplier value |
precision | number | 0 | 2 | Decimal 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.
const menu = showGameMenu({
container: document.getElementById('menuContainer'),
helpContent: '<p><strong>My Game</strong></p><p>Game instructions here.</p>',
});
// Later: menu.destroy() to removeOptions
| Option | Type | Required | Description |
|---|---|---|---|
container | HTMLElement | No | Container to mount the menu bar into. If omitted, mounts to document.body with position: fixed |
helpContent | string | No | HTML string shown in the help overlay |
helpUrl | string | No | URL loaded in an iframe inside the help overlay (alternative to helpContent) |
onSoundSettings | () => void | No | Custom 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 menucreateSettingsButton({ 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.
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
| Option | Type | Required | Description |
|---|---|---|---|
title | string | No | Overlay title text |
content | string | HTMLElement | Yes | HTML string or DOM element to display in the overlay body |
onClose | () => void | Yes | Called 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.
import { playVideo } from 'rollerz-framework';
await playVideo({
url: '/assets/intro.mp4',
muted: false,
});
// Video finished — continue with gameOptions
| Option | Type | Default | Description |
|---|---|---|---|
url | string | — | Video source URL |
parent | HTMLElement | document.body | Container to attach the video overlay to |
muted | boolean | false | Start muted |
loop | boolean | false | Loop the video (click to dismiss still works) |
holdLastFrame | boolean | false | Keep 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
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 removeEach entry in providers needs:
name— display label in the panelprovider— a provider instance (e.g.sdk.go3) that exposesdevConfig,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:
provider.setDevEnabled(true)is called, which initializes the dev state todevConfig.defaultState- Control changes call
provider.setDevState({ key: value })to update individual state fields - When
placeBetis called on that provider, it skips the API call and instead callsdevConfig.generateBetResponse(betAmount, betType, devState)to produce a local result collectalso runs locally, updating the session balance from the dev result
DevProviderConfig
Providers define their dev behavior via devConfig:
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:
| Type | Description | Example |
|---|---|---|
radio | Single-select radio buttons | Count: 1, 2, 3 |
slider | Numeric slider with min/max/step | Multiplier: 1–5 |
checkbox | Boolean toggle | Force bonus: on/off |
text | Free-text input | Custom round ID |
multiselect | Multi-select checkboxes | Outcomes: forceLoss, forceWin, forceBonus |
groupedCounters | Repeatable rows of per-key counters that must sum to a fixed slot count | Card packs: C/U/R/L counts summing to 5 |
Use the devControl helper to create controls:
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:
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:
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".
| Provider | Controls | Notes |
|---|---|---|
| GO3 | outcome (forceLoss/forceWin/forceBonus), multiplier, count | Single-shot instant reveal — the result is returned directly from placeBet. |
| Stepper | outcome (survive/crashAtStep), crashAtStep, payoutGrowthPercent, totalSteps | placeBet 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. |
| GP | resultJson | Single 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:
generateBetResponsereturns aStepperBetResultwithsteps: [...]precomputed from the bet amount usingbetAmount * (1 + step * payoutGrowthPercent / 100).currentStepstarts at 0 androundEnded: false.difficultyechoes the player'sbetType(no override).step()checksisDevEnabled()and walks the stored_devRound, updatingcurrentStep,currentPayout, and each step'soutcome('SAFE'or'CRASH') based on theoutcomecontrol. The round'sbalancefield is kept in sync with the current cash-out value so a manual collect mid-round applies the latest payout.collect()applies_devRound.balanceto 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:
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:
| Provider | Controls | Notes |
|---|---|---|
| Rippin Rumble | packs (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.