Appearance
Slot Game
This example shows how to use tags, feature awards, and feature tables. It generates config for a slot game with a free spins bonus feature.
The Game
- 3 reels, 3 rows with a single payline (middle row)
- Symbols: 0–5 (regular, low-to-high) and 6 (scatter)
- Win: 3 of a kind on the payline pays
symbol × 10 - Free spins: 3+ scatters anywhere triggers 10 free spins
Key Concepts
This example demonstrates:
metaTags- Classifies spin outcomes ('no-win','big-win','freespin-trigger')featureparameter - Tags base game and free spin entries with afeaturefield in the output filesfeatureAwards- Tells the hizi engine to award bonus spins that reference a feature name
Full Example
typescript
import { HiziEngineGenerator } from '@hizi.io/engine-generator';
import type { TFeaturesAwarded } from '@hizi.io/engine-generator';
const SYMBOLS = [0, 1, 2, 3, 4, 5, 6]; // 6 = scatter
const SCATTER = 6;
const NUM_REELS = 3;
const NUM_ROWS = 3;
function randomSymbol(): number {
return SYMBOLS[Math.floor(Math.random() * SYMBOLS.length)];
}
function simulateSpin() {
// Generate the grid
const grid: number[][] = [];
let scatterCount = 0;
for (let row = 0; row < NUM_ROWS; row++) {
const rowSymbols: number[] = [];
for (let reel = 0; reel < NUM_REELS; reel++) {
const sym = randomSymbol();
rowSymbols.push(sym);
if (sym === SCATTER) scatterCount++;
}
grid.push(rowSymbols);
}
// Check payline (middle row, index 1)
const payline = [grid[1][0], grid[1][1], grid[1][2]];
let win = 0;
if (payline[0] === payline[1] && payline[1] === payline[2] && payline[0] !== SCATTER) {
win = payline[0] * 10;
}
return { grid, win, scatterCount };
}
async function main() {
const NUM_BASE_SPINS = 1_000_000;
const NUM_FREESPIN_SPINS = 1_000_000;
const generator = new HiziEngineGenerator();
await generator.start('./output/');
// ── Base game feature ──
for (let i = 0; i < NUM_BASE_SPINS; i++) {
const base = simulateSpin();
const triggersFreespins = base.scatterCount >= 3;
// Meta tags
const metaTags: string[] = [];
if (base.win === 0 && !triggersFreespins) metaTags.push('no-win');
if (base.win >= 30) metaTags.push('big-win');
if (triggersFreespins) metaTags.push('freespin-trigger');
// Feature awards config
let featureAwards: TFeaturesAwarded | undefined;
if (triggersFreespins) {
featureAwards = {
type: 'randomChoice',
awards: [
{
count: 10,
feature: 'freespin', // Use the 'freespin' feature tables
},
],
};
}
// Record base game spin
generator.addResult(
{ grid: base.grid },
{
feature: 'basegame',
win: base.win,
metaTags,
featureAwards,
},
);
}
// ── Freespin feature ──
for (let i = 0; i < NUM_FREESPIN_SPINS; i++) {
const free = simulateSpin();
const fsMetaTags: string[] = [];
if (free.win === 0) fsMetaTags.push('no-win');
if (free.win >= 30) fsMetaTags.push('big-win');
// Record freespin result into the freespin tables
generator.addResult(
{ grid: free.grid },
{
feature: 'freespin',
win: free.win,
metaTags: fsMetaTags,
},
);
}
await generator.end();
console.log('Done!');
}
main();How Features and Spins Work Together
The flow during simulation:
entries.jsonl (every feature in one file, sorted by weight descending)
{"feature":"freespin","id":7,"weight":985372,"cumulativeWeight":985372,"win":0,"metaTags":["no-win"]}
{"feature":"basegame","id":0,"weight":866504,"cumulativeWeight":866504,"win":0,"metaTags":["no-win"]}
{"feature":"basegame","id":1,"weight":119043,"cumulativeWeight":985547,"win":0,"metaTags":["freespin-trigger"],
"featureAwards":{"type":"randomChoice","awards":[{"count":10,"feature":"freespin"}]}}
{"feature":"freespin","id":12,"weight":2955,"cumulativeWeight":988327,"win":50,"metaTags":["big-win"]}
{"feature":"freespin","id":8,"weight":2934,"cumulativeWeight":991261,"win":10,"metaTags":[]}
{"feature":"basegame","id":2,"weight":2911,"cumulativeWeight":988458,"win":10,"metaTags":[]}
…The generator sorts all entries by weight, from highest to lowest, before it writes them. Rows of different features therefore interleave. cumulativeWeight stays a per-feature running total, so it does not increase monotonically down the file.
At runtime, the hizi engine:
- Picks a base game entry (where
feature === 'basegame') using the weighted distribution. - If the entry has
featureAwardswithfeature: 'freespin', it runs that many spins using free spin entries.
Output Structure
The generator produces two output files: entries.jsonl and scenarios.jsonl.
The tables below list the entries from one run of 1,000,000 base spins and 1,000,000 free spins. The weights are approximate. A different run produces slightly different numbers, because the example draws every symbol at random.
basegame entries (in entries.jsonl):
| win | weight | metaTags | featureAwards |
|---|---|---|---|
| 0 | ~866,500 | ['no-win'] | - |
| 0 | ~119,000 | ['freespin-trigger'] | 10 spins → 'freespin' entries |
| 10 | ~2,900 | [] | - |
| 20 | ~2,900 | [] | - |
| 30 | ~2,900 | ['big-win'] | - |
| 40 | ~2,900 | ['big-win'] | - |
| 50 | ~2,900 | ['big-win'] | - |
freespin entries (in entries.jsonl):
| win | weight | metaTags | featureAwards |
|---|---|---|---|
| 0 | ~985,400 | ['no-win'] | - |
| 10 | ~2,900 | [] | - |
| 20 | ~2,900 | [] | - |
| 30 | ~2,900 | ['big-win'] | - |
| 40 | ~2,900 | ['big-win'] | - |
| 50 | ~2,900 | ['big-win'] | - |
The win ladder has five steps, because a payline of three matching symbols pays symbol × 10 and the five paying symbols are 1 to 5. Three matching 0 symbols pay 0, so they land in the no-win entry. The freespin loop never tags freespin-trigger, because it ignores the scatter count.
The generator combines all features in the same files. The feature field on each entry distinguishes them.