Skip to content

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')
  • feature parameter - Tags base game and free spin entries with a feature field in the output files
  • featureAwards - 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:

  1. Picks a base game entry (where feature === 'basegame') using the weighted distribution.
  2. If the entry has featureAwards with feature: '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):

winweightmetaTagsfeatureAwards
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):

winweightmetaTagsfeatureAwards
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.