Appearance
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| Parameter | Type | Description |
|---|---|---|
entries | IKpiEntry[] | Entries for the KPI computation (typically base game entries) |
baseEntries | IKpiEntry[] | Base game entries (for calculating the game RTP) |
featureEntriesMap | Map<string, IKpiEntry[]> | Map of feature name to entries (e.g. 'freespin' maps to freespin entries) |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs (for RTP of counter-triggered features) |
wagerFeatures | Set<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) |
playerChoiceAnalysis | boolean | When 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| Parameter | Type | Description |
|---|---|---|
baseEntries | IKpiEntry[] | Base game entries |
featureEntriesMap | Map<string, IKpiEntry[]> | Map of feature name to entries |
requiredFeatures | Set<string> | All feature names referenced by feature awards |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs |
wagerFeatures | Set<string> | Feature names that use wager mechanics, excluded from the additive RTP contribution. Pass the union of the game config's wagerFeatures and wagerStakeFeatures |
playerChoiceAnalysis | boolean | When 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,
computeGameRtpadds them explicitly. For each qualifying counter:expectedCompletionscomes from one of two models, chosen byresetOnFeatureEnd(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
progressionInfoat 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 })[]| Parameter | Type | Description |
|---|---|---|
baseEntries | T[] | Base game entries |
featureEntriesMap | Map<string, IKpiEntry[]> | Map of feature name to entries |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs |
wagerFeatures | Set<string> | Feature names that use wager mechanics, excluded from the effective win bonus. Pass the union of the game config's wagerFeatures and wagerStakeFeatures |
playerChoiceAnalysis | boolean | When 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| Parameter | Type | Default | Description |
|---|---|---|---|
baseEntries | IKpiEntry[] | - | Base game entries |
featureEntriesMap | Map<string, IKpiEntry[]> | - | Map of feature name to entries. Keys use the sanitized feature name |
options.maxRetriggerDepth | number | 0 | How 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.wagerFeatures | Set<string> | - | Feature names that use relative wager mechanics. The function excludes them from the additive payout |
options.wagerStakeFeatures | Set<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.buyFeatures | IBuyFeaturePayoutInput[] | [] | 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>;
}| Field | Type | Description |
|---|---|---|
maxPayout | number | Maximum theoretical win as a multiplier of stake, across all starting paths |
route | IMaxPayoutRouteStep[] | The steps through the game graph that reach maxPayout |
winningPath | string | Which path produced the maximum: 'basegame' or buyfeature:<id> |
buyFeatureMaxPayouts | Record<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 };
}| Field | Type | Description |
|---|---|---|
feature | string | Feature name for this step |
spins | number | Number of spins played in this feature at this step |
bestEntryWin | number | Best 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;
}| Field | Type | Description |
|---|---|---|
id | string | Buy-feature identifier. It appears in winningPath and in buyFeatureMaxPayouts |
featureKey | string | Sanitized 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? | number | Number 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[]): IOverallKpiscomputeMetaTagKpis()
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[]): IWinDistributionEach 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[]): IWinOddsclassifyVolatility()
Classify a standard deviation value into a volatility category.
typescript
classifyVolatility(stdDev: number): VolatilityClass| Range | Classification |
|---|---|
| < 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): stringExamples: 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;
}| Field | Type | Description |
|---|---|---|
weight | number | How many times this outcome occurred |
win? | number | Total win for this entry, as a multiplier of stake |
metaTags? | string[] | Classification labels |
featureAwards? | TFeaturesAwarded | Feature awards that this entry triggers |
progressionAwards? | Record<string, Record<number, number>> | Progression counter increments (counter → step index → increment) |
progressionInfo? | IProgressionInfo | Feature-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;
}| Field | Type | Description |
|---|---|---|
totalEntries | number | Number of unique entries |
totalWeight | number | Sum of all weights |
rtp | number | Return to Player percentage |
hitRate | number | Percentage of spins with win > 0 |
volatilityStdDev | number | Standard deviation of win distribution |
volatilityClass | VolatilityClass | 'Low' | 'Medium' | 'High' | 'Very High' |
maxWin | number | Highest win amount |
IGameRtp
typescript
interface IGameRtp {
baseRtp: number;
featureBreakdowns: IFeatureRtpBreakdown[];
cashCounterBreakdowns: ICashCounterRtpBreakdown[];
totalRtp: number;
allFeaturesLoaded: boolean;
minTotalRtp?: number;
maxTotalRtp?: number;
warnings?: string[];
}| Field | Type | Description |
|---|---|---|
baseRtp | number | Base game RTP percentage |
featureBreakdowns | IFeatureRtpBreakdown[] | Per-feature RTP contributions (sorted by contribution descending) |
cashCounterBreakdowns | ICashCounterRtpBreakdown[] | 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 |
totalRtp | number | Base + all feature contributions + all cash counter contributions |
allFeaturesLoaded | boolean | Whether all detected features had data available |
minTotalRtp | number? | Min possible total RTP (player picks worst option). Present when playerChoiceAnalysis is enabled. |
maxTotalRtp | number? | Max possible total RTP (player picks best option). Present when playerChoiceAnalysis is enabled. |
warnings | string[]? | 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;
}| Field | Type | Description |
|---|---|---|
featureName | string | Feature name (e.g. 'freespin') |
triggerRate | number | Probability of triggering (0–1) |
avgInitialSpins | number | Weighted average initial spins awarded |
evPerSpin | number | Expected value per feature spin (in stake units) |
pRetrigger | number | Retrigger probability within the feature |
expectedTotalSpins | number | Average total spins including retriggers |
rtpContribution | number | Percentage points contributed to total game RTP |
minRtpContribution | number? | Min contribution when player picks worst option. Present when playerChoiceAnalysis is enabled. |
maxRtpContribution | number? | Max contribution when player picks best option. Present when playerChoiceAnalysis is enabled. |
ICashCounterRtpBreakdown
typescript
interface ICashCounterRtpBreakdown {
counterName: string;
rtpContribution: number;
minRtpContribution?: number;
maxRtpContribution?: number;
}| Field | Type | Description |
|---|---|---|
counterName | string | Progression counter name |
rtpContribution | number | Percentage points that this counter's cash payouts contribute to total game RTP |
minRtpContribution | number? | Min contribution when player picks worst option. Present when playerChoiceAnalysis is enabled. |
maxRtpContribution | number? | 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;
}| Field | Type | Description |
|---|---|---|
threshold | number | Win multiplier threshold (e.g. 2, 5, 10) |
odds | string | Human-readable odds (e.g. "1 in 5") |
probability | number | Probability 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[];
}| Field | Type | Description |
|---|---|---|
name | string | Counter name (must match keys in progressionAwards) |
onComplete | object | What happens when the counter reaches 1.0 |
onComplete.type | string | 'playerChoice' or 'randomChoice' |
onComplete.awards | array | Mixed list of feature-spin ({ count, feature }) and cash ({ winMultiplier }) options awarded on completion |