Skip to content

Buy-Features

Buy-features let players purchase direct entry into a bonus feature (e.g. freespins) for a fixed price, bypassing the base-game trigger. The generator's job is to produce the selection pool each purchase draws from, and to write the matching pricing configuration into config.json.

This page covers how to define buy-features at generation time. For how the engine serves them at runtime, see SDK · Buy-Features.

Two parts of a buy-feature

A buy-feature in the generator has two pieces:

  1. Selection pool — an IBuyFeatureEntry. At end() the generator materialises it into entries.jsonl as a synthetic feature named bf_<sanitized-id>. This is the list of entries (with weights) the engine draws from when a player buys the feature.
  2. Pricing config — an IBuyFeatureConfig attached to config.buyFeatures in config.json. This tells the engine the feature's targetPrice, targetRtp, and initialSpins.

You generally want both — one without the other is either an unpriced pool or a priced feature with nothing to select from.

Wiring the pricing config to the pool

Each resolved pool becomes a feature named bf_<sanitized-id> in entries.jsonl (hyphens and spaces become underscores, e.g. buy-freespinbf_buy_freespin). Set IBuyFeatureConfig.feature to this name so the engine's placeBet draws from the pool when a player buys.

Scenarios are shared, not duplicated

A bf_<id> pool entry reuses its source entry's id, and scenarios are keyed by entryId (not by feature). So materialising a pool adds only the lightweight weight rows to entries.jsonl — the scenario data is stored once and shared. A pool entry can therefore re-weight an existing outcome but never invent a new one.

Resolution strategy: entrypool

Buy-feature pools are built by resolving an IBuyFeatureDefinition against the entries your simulation has recorded. The entrypool strategy includes only entries with at least one matching metaTag — the typical pattern for a "buy directly into the bonus" feature: tag your trigger entries, then pool them.

typescript
// All entries tagged 'freespin-trigger' form the pool.
// Each entry keeps its original weight (scaled by metaTagWeights, if set).
{ id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] }

metaTagWeights

Scales tagged entries' weights by a per-tag multiplier, letting you weight one tag's entries relative to another within the pool. An entry matching several selected tags uses the highest weighting; unlisted tags default to 1.

typescript
// Pool = all 'trigger' and 'jackpot' entries. Within it, 'jackpot' entries are
// weighted 5× relative to plain 'trigger' entries.
{ id: 'buy-bonus', type: 'entrypool', metaTags: ['trigger', 'jackpot'], metaTagWeights: { 'jackpot': 5 } }

TIP

A uniform multiplier over a single-tag pool has no effect on selection — every pool entry scales equally, so their relative probabilities are unchanged (and the total scales with them). metaTagWeights only changes outcomes when the pool spans more than one tag. To re-weight individual entries, use weightOverrides.

weightOverrides

Replaces computed weights for specific tagged entries. Useful for fine-tuning the pool's RTP after inspection:

typescript
{
  id: 'buy-freespin',
  type: 'entrypool',
  metaTags: ['freespin-trigger'],
  weightOverrides: { '3': 500, '7': 200 }, // entry IDs → explicit weight
}

Pool scope

Definitions resolve against all entries in the generator, across every feature — an entrypool matches tagged entries regardless of which feature they belong to. If you want a single-feature pool, tag only entries from that feature.

Setting them up

There are two equivalent ways to register a buy-feature with the generator. Pick whichever fits your flow.

Option A — inline via end({ buyFeatureDefinitions })

Pass definitions directly to end(); the generator resolves them against the recorded entries and materialises everything in one shot.

typescript
import { HiziEngineGenerator } from '@hizi.io/engine-generator';
import type { IBuyFeatureDefinition } from '@hizi.io/engine-generator';

const gen = new HiziEngineGenerator();
await gen.start('./output/');

// ...run your simulation, calling gen.addResult(...) with metaTags...

const buyFeatureDefinitions: IBuyFeatureDefinition[] = [
  { id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
  { id: 'buy-boost',    type: 'entrypool', metaTags: ['big-win'] },
];

await gen.end({
  config: {
    gameCode: 'my-slot',
    gameType: 'slot',
    stakes: [1.0],
    features: ['freespin'],
    buyFeatures: [
      { id: 'buy-freespin', feature: 'bf_buy_freespin', targetRtp: 95, targetPrice: 100 },
      { id: 'buy-boost',    feature: 'bf_buy_boost',    targetRtp: 95, targetPrice: 20  },
    ],
  },
  buyFeatureDefinitions,
});

Option B — explicit via buildBuyFeatures()

Resolve the pool yourself before end(), inspect or post-process it, then pass the resolved IBuyFeatureEntry[] as end({ buyFeatures }).

typescript
const buyFeatures = gen.buildBuyFeatures([
  { id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
  { id: 'buy-boost',    type: 'entrypool', metaTags: ['big-win'] },
]);

// Optional: inspect or adjust buyFeatures here

await gen.end({
  config: { /* …as above… */ },
  buyFeatures,
});

buildBuyFeatures() is pure — it resolves and returns the pools without writing anything. The pools are materialised into entries.jsonl only when you pass them to end() (as buyFeatures, or by handing the definitions straight to end({ buyFeatureDefinitions })).

Use Option A for simplicity and Option B when you need to see the resolved pool before committing to it (e.g. to log per-entry weights, compute the buy-feature's RTP, or merge with hand-written entries).

Mixing pre-resolved and auto-resolved

end() accepts both buyFeatures and buyFeatureDefinitions in the same call. Resolved definitions are appended to the pre-resolved list, so you can hand-write a pool for one feature and auto-resolve another. Both land in entries.jsonl as bf_<id> features.

Full example

A minimal simulation with a freespin feature plus two buy-features. This is runnable end-to-end.

typescript
import { HiziEngineGenerator } from '@hizi.io/engine-generator';
import type { IBuyFeatureDefinition, IEndOptions } from '@hizi.io/engine-generator';

const gen = new HiziEngineGenerator({ maxScenariosPerEntry: 5 });
await gen.start('./output/');

// ── Basegame ──
for (let i = 0; i < 600; i++)
  gen.addResult({ reels: [0, 0, 0] }, { feature: 'basegame', win: 0, metaTags: ['no-win'] });
for (let i = 0; i < 300; i++)
  gen.addResult({ reels: [1, 1, 0] }, { feature: 'basegame', win: 2, metaTags: ['small-win'] });
for (let i = 0; i < 60; i++)
  gen.addResult(
    { reels: [7, 7, 7] },
    {
      feature: 'basegame',
      win: 0,
      metaTags: ['freespin-trigger'],
      featureAwards: { type: 'randomChoice', awards: [{ count: 10, feature: 'freespin' }] },
    },
  );
for (let i = 0; i < 40; i++)
  gen.addResult({ reels: [5, 5, 5] }, { feature: 'basegame', win: 20, metaTags: ['big-win'] });

// ── Freespin ──
for (let i = 0; i < 70; i++)
  gen.addResult({ reels: [0, 0, 0] }, { feature: 'freespin', win: 0, metaTags: ['no-win'] });
for (let i = 0; i < 25; i++)
  gen.addResult({ reels: [2, 2, 0] }, { feature: 'freespin', win: 3, metaTags: ['small-win'] });
for (let i = 0; i < 5; i++)
  gen.addResult({ reels: [5, 5, 5] }, { feature: 'freespin', win: 30, metaTags: ['big-win'] });

const buyFeatureDefinitions: IBuyFeatureDefinition[] = [
  { id: 'buy-freespin', type: 'entrypool', metaTags: ['freespin-trigger'] },
  { id: 'buy-boost',    type: 'entrypool', metaTags: ['big-win'] },
];

const endOptions: IEndOptions = {
  config: {
    gameCode: 'buyfeatures-demo',
    gameType: 'slot',
    stakes: [1.0],
    features: ['freespin'],
    buyFeatures: [
      { id: 'buy-freespin', feature: 'bf_buy_freespin', targetRtp: 95, targetPrice: 100 },
      { id: 'buy-boost',    feature: 'bf_buy_boost',    targetRtp: 95, targetPrice: 20  },
    ],
  },
  buyFeatureDefinitions,
};

await gen.end(endOptions);

What gets written

  • entries.jsonl.br — the base entries, plus the materialised bf_<id> pools. For the example above:

    • bf_buy_freespin — 1 entry (the basegame freespin-trigger entry), total weight 60.
    • bf_buy_boost — the big-win entries (basegame + freespin) at their original weights.

    Pool entries reuse their source entry's id, so they share scenarios — no scenario data is duplicated.

  • config.json — includes buyFeatures with the pricing configs above, and an auto-populated featureWeights that adds the bf_<id> features for each pool so their RTP can be computed alongside normal features.

There is no buyfeatures.jsonl — pools live in entries.jsonl, which is what the engine reads at runtime.