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 it does, the entry's weight is incremented. If not, a new entry is created.
Weight
The weight field on an entry represents 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 will have weight: 600000.
Scenarios
A scenario is an array of game state snapshots for a particular occurrence of an entry. Each snapshot is a Record<string, unknown> containing whatever data your engine needs to recreate the outcome visually - reel positions, visible symbols, etc.
Single-Result Scenarios (Recommended)
Most of the time, each scenario contains a single result. You pass a plain Record<string, unknown> to addResult() and the library wraps it in an array automatically:
// 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, producing 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, sticky symbols where the symbol positions on spin N depend on what happened on spins 1 through N-1. In these cases, you can pass an array of records to addResult():
// 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, giving 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, so 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 and cannot be separated.
Scenario Limits
Each entry stores an array of scenarios. By default, up to 1,000 scenarios are kept per entry. After that limit, new occurrences still increment the weight but don't add scenario data. This is configurable:
const generator = new HiziEngineGenerator({
maxScenariosPerEntry: 500, // Store fewer scenarios per entry
});Why limit scenarios?
A "no-win" entry might appear millions of times. Storing every single game state would use enormous amounts of memory. 1,000 representative snapshots is 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 — the kind needed to reach jackpot-odds wins — keep scenarios.jsonl small without losing any entry or weight precision:
maxScenariosPerZeroWinEntry(default2000) — a separate, larger cap for zero-win dead spins. They're ~60% of spin volume in a typical slot, so 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) then share one scenario budget. Entries are still keyed and weighted individually, and every entry keeps at least one scenario so it stays replayable — only redundant example boards are pooled away.
const generator = new HiziEngineGenerator({
maxScenariosPerEntry: 1000,
maxScenariosPerZeroWinEntry: 2000,
looseScenarioCap: true,
});Why pool the cap?
Incidental metaTags can fragment one win into many entries, each storing up to maxScenariosPerEntry boards. On a 100M-spin run that 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 while the jackpot outcomes you ran all those spins for stay fully represented.
Tags
Meta Tags
Meta tags classify what happened on a spin. They're useful for filtering and analytics:
// 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, say, a "big-win" and a "big-win with scatter".
Features
Each game feature (base game, freespins, 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 no feature is passed in the addResult options, it defaults to 'basegame'.
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, entries are filtered 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:
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
There are two feature award wrapper types, distinguished by the type discriminator on the TFeaturesAwarded object:
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. No weighting needed.
// 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. freespins) for a fixed price, bypassing the base-game trigger. Each option maps a featureToBuy id to a different feature table, with the price pre-calculated so the buy-feature RTP matches the target game RTP.
Buy-features live on the game config, so the frontend discovers them via loadConfig — every placeBet that includes a featureToBuy id routes to that feature's entries instead of the base game.
// 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 — a plain placeBet would have 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 is named and accumulates toward 1.0 (100%); when it hits the threshold it fires an award (e.g. freespins) and resets, carrying any excess. Counters are persisted 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 — e.g.{ 'scatter-collection': 0.01 }means "each draw of this entry increments thescatter-collectioncounter by 1%".
await generator.addResult(scenario, {
win,
metaTags,
progressionAwards: { 'scatter-collection': 0.01 },
});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 counter config shape, payload fields, and completion flow.