Skip to content

KPI Math

The generator package exports pure functions that compute game statistics from generated entry data. Examples of these statistics are RTP, volatility, hit rate, and win distribution. Use these functions to validate simulation output. Use these functions to produce compliance reports.

Import

You can import KPI functions from the main package or from a dedicated @hizi.io/engine-generator/math sub-export. Use the sub-export when you need only the math utilities, not the full generator:

typescript
// From the main package
import {
  computeAllKpis,
  computeGameRtp,
  computeEffectiveEntries,
  computeOverallKpis,
  computeMetaTagKpis,
  computeWinDistribution,
  computeWinOdds,
  computeMaxPayout,
  classifyVolatility,
  generateBucketName,
} from '@hizi.io/engine-generator';

// Or from the math sub-export
import { computeAllKpis, computeGameRtp } from '@hizi.io/engine-generator/math';

Functions

computeAllKpis()

Compute a full KPI suite in one call. The function includes a game RTP breakdown when you provide feature data.

typescript
computeAllKpis(
  entries: IKpiEntry[],
  baseEntries?: IKpiEntry[],
  featureEntriesMap?: Map<string, IKpiEntry[]>,
  progressionCounters?: IProgressionCounterConfig[],
  wagerFeatures?: Set<string>,
  playerChoiceAnalysis?: boolean
): IAllKpis
ParameterTypeDescription
entriesIKpiEntry[]Entries for the KPI computation (typically base game entries)
baseEntriesIKpiEntry[]Base game entries (for calculating the game RTP)
featureEntriesMapMap<string, IKpiEntry[]>Map of feature name to entries (e.g. 'freespin' maps to freespin entries)
progressionCountersIProgressionCounterConfig[]Progression counter configs (for RTP of counter-triggered features)
wagerFeaturesSet<string>Feature names that use wager mechanics, excluded from the additive RTP contribution. Pass the union of the game config's wagerFeatures (relative multipliers) and wagerStakeFeatures (absolute cashout multipliers)
playerChoiceAnalysisbooleanWhen true, the function computes a min/max RTP range based on the player choice strategy. It adds minTotalRtp/maxTotalRtp to IGameRtp and per-feature minRtpContribution/maxRtpContribution.
typescript
import { readFileSync } from 'fs';
import { brotliDecompressSync } from 'zlib';
import { HiziEngineGenerator, computeAllKpis } from '@hizi.io/engine-generator';

const entriesJsonl = brotliDecompressSync(readFileSync('./output/entries.jsonl.br')).toString();
const scenariosJsonl = brotliDecompressSync(readFileSync('./output/scenarios.jsonl.br')).toString();

const baseEntries = HiziEngineGenerator.loadEntries(entriesJsonl, scenariosJsonl, 'basegame');
const freespinEntries = HiziEngineGenerator.loadEntries(entriesJsonl, scenariosJsonl, 'freespin');

const featureMap = new Map([['freespin', freespinEntries]]);
const kpis = computeAllKpis(baseEntries, baseEntries, featureMap);

console.log(`RTP: ${kpis.overall.rtp}%`);
console.log(`Hit Rate: ${kpis.overall.hitRate}%`);
console.log(`Volatility: ${kpis.overall.volatilityClass}`);

if (kpis.gameRtp) {
  console.log(`Total Game RTP: ${kpis.gameRtp.totalRtp}%`);
  for (const f of kpis.gameRtp.featureBreakdowns) {
    console.log(`  ${f.featureName}: ${f.rtpContribution}%`);
  }
}

computeGameRtp()

Compute the RTP breakdown per feature and the overall game RTP, including retrigger expansion and progression counter contributions.

typescript
computeGameRtp(
  baseEntries: IKpiEntry[],
  featureEntriesMap: Map<string, IKpiEntry[]>,
  requiredFeatures: Set<string>,
  progressionCounters?: IProgressionCounterConfig[],
  wagerFeatures?: Set<string>,
  playerChoiceAnalysis?: boolean
): IGameRtp
ParameterTypeDescription
baseEntriesIKpiEntry[]Base game entries
featureEntriesMapMap<string, IKpiEntry[]>Map of feature name to entries
requiredFeaturesSet<string>All feature names referenced by feature awards
progressionCountersIProgressionCounterConfig[]Progression counter configs
wagerFeaturesSet<string>Feature names that use wager mechanics, excluded from the additive RTP contribution. Pass the union of the game config's wagerFeatures and wagerStakeFeatures
playerChoiceAnalysisbooleanWhen true, the function computes a min/max RTP range for player choice scenarios

The function returns an IGameRtp object. This object has baseRtp, per-feature breakdowns, and cashCounterBreakdowns. cashCounterBreakdowns has one entry per counter whose awards array contains at least one cash option, and whose accumulated increment mass is not 0. The function skips a counter that never accumulates anything anywhere. A counter that accumulates mass but never completes (a reset always wins) still gets a row, so its 0 contribution stays visible. The object also has totalRtp (= baseRtp + Σ featureContributions + Σ cashCounterContributions).

A counter's onComplete.awards array may mix feature and cash options. The computeGameRtp function handles each option independently:

  • Feature options contribute through the empirical feature breakdowns. The counter-awarded spins end up in the target feature's database (DB). The empirical RTP of that feature picks them up automatically.
  • Cash options never enter any DB. Therefore, computeGameRtp adds them explicitly. For each qualifying counter:
    • expectedCompletions comes from one of two models, chosen by resetOnFeatureEnd (see below).
    • expectedCashPerCompletion = Σ(option.winMultiplier × selectionFraction) summed over the cash options only (feature options contribute 0 cash here).
    • rtpContribution = expectedCompletions × expectedCashPerCompletion / baseTotalWeight × 100.

Two models for expectedCompletions

A counter without resetOnFeatureEnd persists across gamerounds, so every unit of increment mass eventually completes. The function uses the total mass:

expectedCompletions = Σ(entry.weight × Σ_step progressionAwards[counter][step])

summed across the base DB and every feature DB. The function sums each entry's per-step deltas for the counter first, then weights them.

A counter with resetOnFeatureEnd discards the mass that sits on the meter when a resetting feature ends, so the total-mass model overstates it. The function instead walks each entry deterministically from the baked progressionInfo: it applies the entry's progressionEvents in step order, counts one completion each time the value reaches 1.0 (and subtracts 1.0), and resets the value to 0 at each featureEnds step that names a resetting feature. Increments at a reset step apply before the reset, which matches the runtime order.

The walk starts each entry at 0. That assumption holds only when every entry also ends at 0. The function therefore falls back to the total-mass model for the whole counter in two cases:

  • an entry leaves a non-zero residual on the counter after its events and resets, or
  • an entry increments the counter but carries no progressionInfo at all.

The fallback figure is an upper bound on the counter's cash contribution, not an exact number. The function then calls console.warn and pushes a message describing the offending entry onto IGameRtp.warnings. Check that field before you treat a reset-scoped counter's contribution as exact.

Player-choice min/max for a cash counter breakdown collapses to min/max over option.winMultiplier if isCash(option) else 0. Picking a feature option contributes 0 cash to this breakdown. Its expected value (EV) flows through the feature breakdowns instead.

selectionFraction assumes uniform choice, not real player behaviour

selectionFraction is the weight given to each option of an onComplete/featureAwards array. The generator uses selectionFraction when it computes a single blended RTP number. For randomChoice, selectionFraction is proportional to weighting. For playerChoice, selectionFraction is uniform (1 / awards.length). For example, take a playerChoice counter with options [{ count: 10, feature: 'freespins' }, { winMultiplier: 25 }]. The generator scores this counter as if a player picks each option 50% of the time, regardless of how players actually choose.

That default number is a modelling convenience. It is not a player-optimal figure. If real players lean toward one option, pass playerChoiceAnalysis: true instead. This gives you minRtpContribution/maxRtpContribution (and minTotalRtp/maxTotalRtp). These values show the true worst-case bound (player always picks the lowest-value option) and the best-case bound. The generator evaluates each option independently, rather than assuming a uniform choice. The same method applies to computeEffectiveEntries() below.


computeEffectiveEntries()

Compute an "effective win" for each base entry. The effective win includes the expected value of any awarded feature spins. This value is useful for auto-balancing.

typescript
computeEffectiveEntries<T extends IKpiEntry>(
  baseEntries: T[],
  featureEntriesMap: Map<string, IKpiEntry[]>,
  progressionCounters?: IProgressionCounterConfig[],
  wagerFeatures?: Set<string>,
  playerChoiceAnalysis?: boolean
): (T & { effectiveWin: number; minEffectiveWin?: number; maxEffectiveWin?: number })[]
ParameterTypeDescription
baseEntriesT[]Base game entries
featureEntriesMapMap<string, IKpiEntry[]>Map of feature name to entries
progressionCountersIProgressionCounterConfig[]Progression counter configs
wagerFeaturesSet<string>Feature names that use wager mechanics, excluded from the effective win bonus. Pass the union of the game config's wagerFeatures and wagerStakeFeatures
playerChoiceAnalysisbooleanWhen true, the function adds minEffectiveWin and maxEffectiveWin based on the player choice strategy

The effectiveWin formula works as follows. For each base entry, add win + sum(featureAwards × featureEvPerSpin). Then, for every progression counter increment on this entry, add the option-by-option contribution inc × selectionFraction × (count × featureEvPerSpin for feature options, or winMultiplier for cash options). PlayerChoice min/max evaluate each option independently. Feature options contribute their feature EV. Cash options contribute their winMultiplier.


computeMaxPayout()

Compute the theoretical maximum payout of a single gameround, as a multiplier of stake. This function produces the maxPayout and maxPayoutRoute values in IGameConfig, and the per-buy-feature maxPayout in IBuyFeatureConfig.

typescript
computeMaxPayout(
  baseEntries: IKpiEntry[],
  featureEntriesMap: Map<string, IKpiEntry[]>,
  options?: {
    maxRetriggerDepth?: number;
    wagerFeatures?: Set<string>;
    wagerStakeFeatures?: Set<string>;
    buyFeatures?: IBuyFeaturePayoutInput[];
  }
): IMaxPayoutResult
ParameterTypeDefaultDescription
baseEntriesIKpiEntry[]-Base game entries
featureEntriesMapMap<string, IKpiEntry[]>-Map of feature name to entries. Keys use the sanitized feature name
options.maxRetriggerDepthnumber0How deep the function follows retrigger chains. 0 follows none. Each extra level multiplies the result by the retrigger spin count, so even 1 produces very large numbers on a high-volatility game
options.wagerFeaturesSet<string>-Feature names that use relative wager mechanics. The function excludes them from the additive payout
options.wagerStakeFeaturesSet<string>-Feature names that use absolute cashout mechanics. Each step's win replaces the accumulated win instead of adding to it. The function follows these chains to their natural end, without the maxRetriggerDepth cap
options.buyFeaturesIBuyFeaturePayoutInput[][]Buy-feature starting points to evaluate alongside the base game path

The function evaluates every starting path: the base game, plus one path per buy feature. It takes the best case at each step, for both playerChoice and randomChoice awards. The result is therefore an optimal player with the luckiest draw, not an expected value.

typescript
const result = computeMaxPayout(baseEntries, featureMap, {
  maxRetriggerDepth: 1,
  buyFeatures: [{ id: 'buy-freespin', featureKey: 'freespin', spins: 10 }],
});

console.log(result.maxPayout);            // e.g. 5000  (× stake)
console.log(result.winningPath);          // 'basegame' or 'buyfeature:buy-freespin'
console.log(result.buyFeatureMaxPayouts); // { 'buy-freespin': 5000 }

IMaxPayoutResult

typescript
interface IMaxPayoutResult {
  maxPayout: number;
  route: IMaxPayoutRouteStep[];
  winningPath: string;
  buyFeatureMaxPayouts: Record<string, number>;
}
FieldTypeDescription
maxPayoutnumberMaximum theoretical win as a multiplier of stake, across all starting paths
routeIMaxPayoutRouteStep[]The steps through the game graph that reach maxPayout
winningPathstringWhich path produced the maximum: 'basegame' or buyfeature:<id>
buyFeatureMaxPayoutsRecord<string, number>Maximum payout per buy feature, keyed by buy-feature id

IMaxPayoutRouteStep

typescript
interface IMaxPayoutRouteStep {
  feature: string;
  spins: number;
  bestEntryWin: number;
  awardType?: 'playerChoice' | 'randomChoice';
  chosenAward?: { feature: string; count: number };
}
FieldTypeDescription
featurestringFeature name for this step
spinsnumberNumber of spins played in this feature at this step
bestEntryWinnumberBest single-spin win multiplier found in this feature
awardType?'playerChoice' | 'randomChoice'Type of award selection that led into this feature. Absent for the base game
chosenAward?{ feature: string; count: number }The award option the function chose (the best one)

IBuyFeaturePayoutInput

typescript
interface IBuyFeaturePayoutInput {
  id: string;
  featureKey: string;
  spins?: number;
}
FieldTypeDescription
idstringBuy-feature identifier. It appears in winningPath and in buyFeatureMaxPayouts
featureKeystringSanitized key into featureEntriesMap. For an entrypool buy feature this is bf_<id>. For a feature type buy feature it is the target feature name
spins?numberNumber of spins awarded on entry. Set the configured initialSpins for a feature type. Omit it for an entrypool type, which the function treats as one spin from the filtered pool

computeOverallKpis()

Compute overall statistics: RTP, hit rate, volatility, and max win.

typescript
computeOverallKpis(entries: IKpiEntry[]): IOverallKpis

computeMetaTagKpis()

Compute per-tag statistics: frequency, RTP contribution, average win, etc.

typescript
computeMetaTagKpis(entries: IKpiEntry[]): IMetaTagKpis[]

computeWinDistribution()

Compute a bucketed win distribution. The function always returns the same six buckets, from lowest win to highest: "0x", "0–1x", "1–5x", "5–20x", "20–100x", "100x+".

typescript
computeWinDistribution(entries: IKpiEntry[]): IWinDistribution

Each bucket is exclusive at its lower bound and inclusive at its upper bound. "0–1x" therefore holds wins above 0 and up to 1.


computeWinOdds()

Compute win odds at standard thresholds (e.g. "1 in 5 chance of winning 2x or more").

typescript
computeWinOdds(entries: IKpiEntry[]): IWinOdds

classifyVolatility()

Classify a standard deviation value into a volatility category.

typescript
classifyVolatility(stdDev: number): VolatilityClass
RangeClassification
< 5'Low'
5–10'Medium'
10–15'High'
>= 15'Very High'

generateBucketName()

Generate a human-readable label for a win bucket range. The function rounds a bound that is within 0.01 of a whole number to that whole number, so 0.001 prints as 0 and 5.001 prints as 5.

typescript
generateBucketName(min: number, max: number): string

Examples: generateBucketName(0, 0) returns "0x", generateBucketName(0.001, 1) returns "0–1x", and generateBucketName(100.001, Infinity) returns "100x+".


Types

IKpiEntry

Input type for all KPI functions. It is IEntryConfig without scenarios, so a loaded entry or an entry metadata row fits it directly.

typescript
type IKpiEntry = Omit<IEntryConfig, 'scenarios'>;

// which resolves to:
{
  weight: number;
  win?: number;
  metaTags?: string[];
  featureAwards?: TFeaturesAwarded;
  progressionAwards?: Record<string, Record<number, number>>;
  progressionInfo?: IProgressionInfo;
}
FieldTypeDescription
weightnumberHow many times this outcome occurred
win?numberTotal win for this entry, as a multiplier of stake
metaTags?string[]Classification labels
featureAwards?TFeaturesAwardedFeature awards that this entry triggers
progressionAwards?Record<string, Record<number, number>>Progression counter increments (counter → step index → increment)
progressionInfo?IProgressionInfoFeature-run boundaries and flat per-step counter increments. The reset-aware cash-counter walk reads this field

IAllKpis

Complete KPI result returned by computeAllKpis().

typescript
interface IAllKpis {
  overall: IOverallKpis;
  metaTags: IMetaTagKpis[];
  winDistribution: IWinDistribution;
  winOdds: IWinOdds;
  gameRtp: IGameRtp | null;
}

IOverallKpis

typescript
interface IOverallKpis {
  totalEntries: number;
  totalWeight: number;
  rtp: number;
  hitRate: number;
  volatilityStdDev: number;
  volatilityClass: VolatilityClass;
  maxWin: number;
}
FieldTypeDescription
totalEntriesnumberNumber of unique entries
totalWeightnumberSum of all weights
rtpnumberReturn to Player percentage
hitRatenumberPercentage of spins with win > 0
volatilityStdDevnumberStandard deviation of win distribution
volatilityClassVolatilityClass'Low' | 'Medium' | 'High' | 'Very High'
maxWinnumberHighest win amount

IGameRtp

typescript
interface IGameRtp {
  baseRtp: number;
  featureBreakdowns: IFeatureRtpBreakdown[];
  cashCounterBreakdowns: ICashCounterRtpBreakdown[];
  totalRtp: number;
  allFeaturesLoaded: boolean;
  minTotalRtp?: number;
  maxTotalRtp?: number;
  warnings?: string[];
}
FieldTypeDescription
baseRtpnumberBase game RTP percentage
featureBreakdownsIFeatureRtpBreakdown[]Per-feature RTP contributions (sorted by contribution descending)
cashCounterBreakdownsICashCounterRtpBreakdown[]Per-counter RTP contributions from cash-awarding progression counters (sorted by contribution descending). Feature-awarding counters do not appear here. featureBreakdowns already captures their contribution empirically
totalRtpnumberBase + all feature contributions + all cash counter contributions
allFeaturesLoadedbooleanWhether all detected features had data available
minTotalRtpnumber?Min possible total RTP (player picks worst option). Present when playerChoiceAnalysis is enabled.
maxTotalRtpnumber?Max possible total RTP (player picks best option). Present when playerChoiceAnalysis is enabled.
warningsstring[]?Non-fatal accuracy warnings. Absent when every figure is exact. A counter that falls back to the total-mass model pushes a message here. See computeGameRtp()

IFeatureRtpBreakdown

typescript
interface IFeatureRtpBreakdown {
  featureName: string;
  triggerRate: number;
  avgInitialSpins: number;
  evPerSpin: number;
  pRetrigger: number;
  expectedTotalSpins: number;
  rtpContribution: number;
  minRtpContribution?: number;
  maxRtpContribution?: number;
}
FieldTypeDescription
featureNamestringFeature name (e.g. 'freespin')
triggerRatenumberProbability of triggering (0–1)
avgInitialSpinsnumberWeighted average initial spins awarded
evPerSpinnumberExpected value per feature spin (in stake units)
pRetriggernumberRetrigger probability within the feature
expectedTotalSpinsnumberAverage total spins including retriggers
rtpContributionnumberPercentage points contributed to total game RTP
minRtpContributionnumber?Min contribution when player picks worst option. Present when playerChoiceAnalysis is enabled.
maxRtpContributionnumber?Max contribution when player picks best option. Present when playerChoiceAnalysis is enabled.

ICashCounterRtpBreakdown

typescript
interface ICashCounterRtpBreakdown {
  counterName: string;
  rtpContribution: number;
  minRtpContribution?: number;
  maxRtpContribution?: number;
}
FieldTypeDescription
counterNamestringProgression counter name
rtpContributionnumberPercentage points that this counter's cash payouts contribute to total game RTP
minRtpContributionnumber?Min contribution when player picks worst option. Present when playerChoiceAnalysis is enabled.
maxRtpContributionnumber?Max contribution when player picks best option. Present when playerChoiceAnalysis is enabled.

IMetaTagKpis

typescript
interface IMetaTagKpis {
  tag: string;
  entryCount: number;
  totalWeight: number;
  frequency: number;
  rtp: number;
  rtpContribution: number;
  hitRate: number;
  volatilityStdDev: number;
  averageWin: number;
  maxWin: number;
}

IWinDistribution

typescript
interface IWinDistribution {
  buckets: IWinBucket[];
}

interface IWinBucket {
  label: string;
  probability: number;
  entryCount: number;
}

IWinOdds

typescript
interface IWinOdds {
  thresholds: IWinOddsThreshold[];
}

interface IWinOddsThreshold {
  threshold: number;
  odds: string;
  probability: number;
}
FieldTypeDescription
thresholdnumberWin multiplier threshold (e.g. 2, 5, 10)
oddsstringHuman-readable odds (e.g. "1 in 5")
probabilitynumberProbability as a percentage (0–100)

VolatilityClass

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

type VolatilityClass = 'Low' | 'Medium' | 'High' | 'Very High';

VolatilityClass is the public name. The package declares the type internally as TVolatilityClass and re-exports it under the public name.


IProgressionCounterConfig

Configuration for a progression counter. A progression counter triggers an award on completion: feature spins, cash, or a mix of both. See Types · IProgressionCounterConfig for the full interface (including resetOnFeatureEnd). See Progression Counters for how the generator sets a progression counter at generation time.

typescript
interface IProgressionCounterConfig {
  name: string;
  onComplete: TProgressionAwarded; // { type: 'randomChoice' | 'playerChoice'; awards: (IFeatureAward | ICashAward)[] }
  stakeSpecific?: boolean;
  resetOnFeatureEnd?: string[];
}
FieldTypeDescription
namestringCounter name (must match keys in progressionAwards)
onCompleteobjectWhat happens when the counter reaches 1.0
onComplete.typestring'playerChoice' or 'randomChoice'
onComplete.awardsarrayMixed list of feature-spin ({ count, feature }) and cash ({ winMultiplier }) options awarded on completion