Appearance
Core Concepts
Entries
An entry is a unique combination of:
- Win amount - The payout for this outcome.
- Meta tags - Labels that classify the outcome (e.g.
'big-win','freespins-trigger'). - Feature awards - Configuration for feature awards triggered by this outcome.
When you call addResult(), the library checks if an entry with the same win, metaTags, and featureAwards combination already exists. If the entry exists, the library increases its weight. If not, the library creates a new entry.
Weight
The weight field on an entry shows how many times that exact outcome occurred during your simulation. The hizi engine uses these weights to determine the probability distribution.
For example, if you simulate 1,000,000 spins and a "no-win" outcome occurs 600,000 times, that entry has weight: 600000.
Weights accumulate into a cumulative weight. The engine walks this cumulative weight at spin time to draw an entry. See Cheating for how to read that value off the backoffice and force a specific entry during testing.
Scenarios
A scenario is an array of snapshots of the game state for a particular occurrence of an entry. Each snapshot is a Record<string, unknown>. It contains whatever data your engine needs to recreate the outcome visually, for example reel positions and visible symbols.
Single-Result Scenarios (Recommended)
Most of the time, each scenario contains a single result. You pass a plain Record<string, unknown> to addResult(). The library wraps it in an array automatically:
typescript
// Single result - the library wraps this in [scenario] internally
await generator.addResult({ grid: visibleSymbols }, { feature: 'basegame', win });This is the preferred approach. When each scenario contains one result, the hizi engine has the maximum pool of scenarios to choose from per spin. This produces the greatest variety of visual outcomes at runtime.
Multi-Result Scenarios
Some game features produce progressive, dependent data across a sequence of spins. For example, with sticky symbols, the symbol positions on spin N depend on what happened on spins 1 through N-1. In these cases, pass an array of records to addResult():
typescript
// Multi-result - an array of records representing a sequence of dependent spins
await generator.addResult(
[
{ grid: spin1Grid, stickyPositions: [] },
{ grid: spin2Grid, stickyPositions: [[0, 1]] },
{
grid: spin3Grid,
stickyPositions: [
[0, 1],
[2, 0],
],
},
],
{ feature: 'basegame', win: totalWin, metaTags },
);Use multi-result scenarios only when necessary
Bundling multiple results into a single scenario significantly reduces the permutation space. With single-result scenarios, the engine can independently pick any scenario for each spin. This gives N × N × N × ... possible combinations. With multi-result scenarios, each entry's pool is a fixed set of pre-recorded sequences. The engine picks one whole sequence. The variety is limited to however many sequences you stored (up to maxScenariosPerEntry).
Only use multi-result scenarios when the data across spins is genuinely dependent. You cannot separate this data by spin.
Scenario Limits
Each entry stores an array of scenarios. By default, the generator keeps up to 1,000 scenarios per entry. After that limit, new occurrences still increase the weight. New occurrences do not add scenario data. This is configurable:
typescript
const generator = new HiziEngineGenerator({
maxScenariosPerEntry: 500, // Store fewer scenarios per entry
});Why limit scenarios?
A "no-win" entry can appear millions of times. Storing every single game state would use enormous amounts of memory. 1,000 representative snapshots are usually more than enough for the runtime to pick from.
Zero-win and loose caps 0.3.0
Two options (TypeScript generator) help large simulations keep scenarios.jsonl small without losing any entry or weight precision. Large simulations are often necessary to reach jackpot-odds wins.
maxScenariosPerZeroWinEntry(default2000): a separate, larger cap for zero-win dead spins. Zero-win dead spins make up about 60% of spin volume in a typical slot. They benefit from more retained variety than paying outcomes.looseScenarioCap(defaultfalsein the generator, on in the creator): pools the cap perwin + featureAwards + progressionAwardsbucket instead of per full unique entry. Outcomes that share the same win, feature spins, and progression, but differ only in incidental metaTags (cascade, spin-multiplier, win tier), share one scenario budget. The generator still keys and weights each entry individually. Every entry keeps at least one scenario, so it stays replayable. Only redundant example boards are pooled away.
typescript
const generator = new HiziEngineGenerator({
maxScenariosPerEntry: 1000,
maxScenariosPerZeroWinEntry: 2000,
looseScenarioCap: true,
});Why pool the cap?
Incidental metaTags can fragment one win into many entries. Each entry then stores up to maxScenariosPerEntry boards. On a 100M-spin run, this multiplies the scenarios file many times over for no extra fidelity. Pooling the cap by the meaningful outcome (win + feature spins + progression) keeps the file small. The jackpot outcomes you ran all those spins for stay fully represented.
Tags
Meta Tags
Meta tags classify what happened on a spin. They are useful for filtering and analytics:
typescript
// Tag the outcome based on the result
const metaTags: string[] = [];
if (win === 0) metaTags.push('no-win');
if (win >= 100) metaTags.push('big-win');
if (scatterCount >= 3) metaTags.push('scatter-trigger');
await generator.addResult(scenario, { feature: 'basegame', win, metaTags });Two outcomes with different meta tags become separate entries, even if the win amount is the same. This lets the hizi engine distinguish between, for example, a "big-win" and a "big-win with scatter".
Features
Each game feature (base game, free spins, bonus rounds, etc.) writes to the same output files. The feature parameter on addResult() tags each entry with its feature name (e.g. 'basegame', 'freespin'). If you do not pass a feature in the addResult options, it defaults to 'basegame'.
typescript
const generator = new HiziEngineGenerator();
await generator.start('./output/');
// Base game entries
await generator.addResult(scenario, {
win,
metaTags,
});
// Freespin entries
await generator.addResult(scenario, {
feature: 'freespin',
win,
metaTags,
});
await generator.end();At runtime, the engine filters entries by the feature field. When a spin awards feature spins, the feature field on the award tells the engine which entries to draw from.
Feature Awards
When a spin triggers additional free "spins", you pass a featureAwards configuration:
typescript
await generator.addResult(scenario, {
win,
metaTags: ['freespin-trigger'],
featureAwards: {
type: 'randomChoice',
awards: [
{
count: 10, // Award 10 bonus spins
feature: 'freespin', // Those spins use the 'freespin' feature tables
},
],
},
});The feature field tells the hizi engine which feature tables to draw from during the bonus round.
Player Choice vs. Random Choice
The type discriminator on the TFeaturesAwarded object distinguishes two wrapper types for feature awards:
IFeaturesAwardedRandomChoice(type: 'randomChoice') - The engine picks the bonus option randomly. Individual awards support an optionalweightingfield to bias the selection.IFeaturesAwardedPlayerChoice(type: 'playerChoice') - The player selects which bonus to play. This type does not need aweightingfield.
typescript
// Random choice with weighting
const randomAwards: TFeaturesAwarded = {
type: 'randomChoice',
awards: [
{ count: 5, feature: 'bonus-a', weighting: 3 },
{ count: 10, feature: 'bonus-b', weighting: 1 },
],
};
// Player choice (no weighting)
const playerAwards: TFeaturesAwarded = {
type: 'playerChoice',
awards: [
{ count: 5, feature: 'bonus-a' },
{ count: 10, feature: 'bonus-b' },
],
};Buy-Features
A buy-feature lets the player purchase direct entry into a bonus feature (e.g. free spins) for a fixed price. This bypasses the base-game trigger. Each option maps a featureToBuy id to a different feature table. You set the price in advance so the buy-feature RTP (return to player, the average percentage of stakes paid back to players) matches the RTP of the target game.
Buy-features live on the game config, so the frontend discovers them via loadConfig. Every placeBet call that includes a featureToBuy id routes to that feature's entries instead of the base game.
typescript
// Generator side - configured per game in config.json, e.g.
// buyFeatures: [{ id: 'risk_high', feature: 'freespins_high', price: 100, ... }]
// SDK side - select a buy-feature when placing a bet
await placeBet({ backendURL, token, stake: 100, featureToBuy: 'risk_high' });Games that ship only buy-features with no base-game entries (e.g. frogger, mines, plinko, keno, overunder) require a featureToBuy on every new round. Otherwise, a plain placeBet has nothing to draw against.
See Buy-Features for the full SDK surface and option shape.
Progression Counters
A progression counter tracks player progress across many rounds. Each counter has a name and accumulates toward 1.0 (100%). When a counter reaches the threshold, it fires an award (e.g. free spins). The counter then resets and carries over any excess. The engine persists counters per player, and optionally per stake level.
Setup lives in two places:
- Game config declares the counters and what each counter awards on completion.
- Entries opt in to contributing by setting a
progressionAwardsmap, keyed counter → step → increment. The outer key is the counter name. The inner key is the 0-based index of the scenario step (matching the runtime engine'scurrentScenarioIndex). The value is the fractional increment. For example,{ 'scatter-collection': { 0: 0.01 } }means "each draw of this entry increases thescatter-collectioncounter by 1% on the entry's first scenario step". Omit steps with no increment from the inner map.
typescript
await generator.addResult(scenario, {
win,
metaTags,
progressionAwards: { 'scatter-collection': { 0: 0.01 } },
});Per-step attribution lets the runtime stamp counter changes onto the scenario step that caused them. This lets the frontend animate the counter and any threshold-crossing award at the right moment. A multi-result entry whose scenario fires the counter twice, on steps 2 and 4, passes { 'scatter-collection': { 2: 0.2, 4: 0.2 } }.
Because progression state is per-player, counters produce long-running "collect to win" mechanics on top of the otherwise-stateless fixed-odds model. See Progression Counters for the shape of the counter config, the payload fields, and the completion flow.