Skip to content

Slot Game

A complete example showing how to use tags, feature awards, and feature tables to generate config for a slot game with a freespins 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
  • Freespins: 3+ scatters anywhere triggers 10 freespins

Key Concepts

This example demonstrates:

  • metaTags - Classify spin outcomes ('no-win', 'big-win', 'freespin-trigger')
  • feature parameter - Base game and freespin entries are tagged with a feature field in the output files
  • featureAwards - Tell the hizi engine to award bonus spins referencing a feature name

Full Example

typescript
import { HiziEngineGenerator, 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
  {"feature":"basegame","id":0,"weight":...,"cumulativeWeight":...,"win":0,"metaTags":["no-win"]}
  {"feature":"basegame","id":1,"weight":...,"cumulativeWeight":...,"win":50,"metaTags":["big-win"]}
  {"feature":"basegame","id":2,"weight":...,"cumulativeWeight":...,"win":0,"metaTags":["freespin-trigger"],
      "featureAwards":{"type":"randomChoice","awards":[{"count":10,"feature":"freespin"}]}}
  {"feature":"freespin","id":3,"weight":...,"cumulativeWeight":...,"win":20}
  {"feature":"freespin","id":4,"weight":...,"cumulativeWeight":...,"win":0,"metaTags":["no-win"]}
  {"feature":"freespin","id":5,"weight":...,"cumulativeWeight":...,"win":50,"metaTags":["big-win"]}

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 freespin entries.

Output Structure

Two output files are produced — entries.jsonl and scenarios.jsonl:

basegame entries (in entries.jsonl):

winweightmetaTagsfeatureAwards
0~600K['no-win']-
50~24K['big-win']-
0~500['freespin-trigger']10 spins → 'freespin' entries

freespin entries (in entries.jsonl):

winweightmetaTagsfeatureAwards
0~600K['no-win']-
30~24K['big-win']-

All features are combined in the same files, distinguished by the feature field on each entry.