Appearance
Buy-Features
Buy-features let a player buy direct entry into a bonus feature (for example, free spins) for a fixed price. This bypasses the base-game trigger. The generator produces the selection pool that each buy draws from. The generator also writes the matching pricing configuration into config.json.
This page explains how to define buy-features at generation time. See SDK · Buy-Features for how the engine serves them at runtime.
Two parts of a buy-feature
A buy-feature in the generator has two pieces:
- Selection pool: an
IBuyFeatureEntry. Atend(), the generator materialises it intoentries.jsonlas a synthetic feature namedbf_<sanitized-id>. This is the list of entries (with weights) that the engine draws from when a player buys the feature. - Pricing config: an
IBuyFeatureConfigattached toconfig.buyFeaturesinconfig.json. This tells the engine the feature'stargetPrice,targetRtp, andinitialSpins.
You generally need both. Without one, you get 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. For example, buy-freespin becomes bf_buy_freespin. Set IBuyFeatureConfig.feature to this name. This makes the engine's placeBet draw from the pool when a player buys.
Scenarios are shared, not duplicated
A bf_<id> pool entry reuses its source entry's id. Scenarios are keyed by entryId, not by feature. So materialising a pool adds only the lightweight weight rows to entries.jsonl. The engine stores the scenario data once and shares it. A pool entry can therefore re-weight an existing outcome. It cannot invent a new one.
Resolution strategy: entrypool
The generator builds buy-feature pools by resolving an IBuyFeatureDefinition against the entries that your simulation has recorded. The entrypool strategy includes only entries with at least one matching metaTag. This is 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
metaTagWeights scales the weights of tagged entries by a per-tag multiplier. Use it to weight one tag's entries relative to another within the pool. An entry that matches 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. Their relative probabilities stay unchanged. 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
weightOverrides replaces the computed weights for specific tagged entries. Use it to fine-tune 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 the one that fits your flow.
Option A: Inline via end({ buyFeatureDefinitions })
Pass definitions directly to end(). The generator resolves them against the recorded entries. It materialises everything in one step.
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 as needed. 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 directly to end({ buyFeatureDefinitions })).
Use Option A for simplicity. Use Option B when you need to see the resolved pool before you commit to it (for example, 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. The generator appends resolved definitions to the pre-resolved list. This lets you 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 materialisedbf_<id>pools. For the example above:bf_buy_freespin: 1 entry (the basegamefreespin-triggerentry), total weight 60.bf_buy_boost: thebig-winentries (basegame + freespin) at their original weights.
Pool entries reuse their source entry's
id. This means they share scenarios. No scenario data is duplicated.config.json: includesbuyFeatureswith the pricing configs above. It also includes an auto-populatedfeatureWeightsthat adds thebf_<id>features for each pool. This lets the generator compute their RTP alongside normal features.
There is no buyfeatures.jsonl. Pools live in entries.jsonl. The engine reads this file at runtime.
Related
IBuyFeatureDefinition: definition shapeIBuyFeatureEntry: resolved pool shapeIBuyFeatureConfig: pricing config shapebuildBuyFeatures()andend(): API reference- SDK · Buy-Features: flow for buying at runtime