KPI Math
The generator package exports pure functions for computing game statistics - RTP, volatility, hit rate, win distribution, and more - from generated entry data. Use these to validate simulation output and produce compliance reports.
Import
KPI functions are available from the main package or via a dedicated @hizi.io/engine-generator/math sub-export (useful when you only need the math utilities without the full generator):
// From the main package
import {
computeAllKpis,
computeGameRtp,
computeEffectiveEntries,
computeOverallKpis,
computeMetaTagKpis,
computeWinDistribution,
computeWinOdds,
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. Optionally includes a game RTP breakdown when feature data is provided.
computeAllKpis(
entries: IKpiEntry[],
baseEntries?: IKpiEntry[],
featureEntriesMap?: Map<string, IKpiEntry[]>,
progressionCounters?: IProgressionCounterConfig[],
wagerFeatures?: Set<string>,
playerChoiceAnalysis?: boolean
): IAllKpis| Parameter | Type | Description |
|---|---|---|
entries | IKpiEntry[] | Entries to compute KPIs over (typically base game entries) |
baseEntries | IKpiEntry[] | Base game entries (for game RTP calculation) |
featureEntriesMap | Map<string, IKpiEntry[]> | Map of feature name → entries (e.g. 'freespin' → freespin entries) |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs (for counter-triggered feature RTP) |
wagerFeatures | Set<string> | Feature names that use multiplicative wins (excluded from RTP contribution) |
playerChoiceAnalysis | boolean | When true, computes min/max RTP range based on player choice strategy. Adds minTotalRtp/maxTotalRtp to IGameRtp and per-feature minRtpContribution/maxRtpContribution. |
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.
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[]> | Feature name → entries |
requiredFeatures | Set<string> | All feature names referenced by feature awards |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs |
wagerFeatures | Set<string> | Feature names that use multiplicative wins (excluded from RTP contribution) |
playerChoiceAnalysis | boolean | When true, computes min/max RTP range for player choice scenarios |
Returns an IGameRtp with baseRtp, per-feature breakdowns, cashCounterBreakdowns (one entry per counter whose awards array contains at least one cash option), and totalRtp (= baseRtp + Σ featureContributions + Σ cashCounterContributions).
A counter's onComplete.awards array may mix feature and cash options. The math handles each option independently:
- Feature options contribute through the empirical feature breakdowns — the counter-awarded spins end up in the target feature's DB, so its empirical RTP picks them up automatically.
- Cash options never enter any DB, so
computeGameRtpadds them explicitly. For each counter with at least one cash option:weightedIncrement = Σ(entry.weight × progressionAwards[counter])summed across base + every feature DB.expectedCashPerCompletion = Σ(option.winMultiplier × selectionFraction)summed over the cash options only (feature options contribute 0 cash here).rtpContribution = weightedIncrement × expectedCashPerCompletion / baseTotalWeight × 100.
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 EV flows through the feature breakdowns instead).
computeEffectiveEntries()
For each base entry, compute an "effective win" that includes the expected value of any awarded feature spins. Useful for auto-balancing.
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[]> | Feature name → entries |
progressionCounters | IProgressionCounterConfig[] | Progression counter configs |
wagerFeatures | Set<string> | Feature names that use multiplicative wins (excluded from effective win bonus) |
playerChoiceAnalysis | boolean | When true, adds minEffectiveWin and maxEffectiveWin based on player choice strategy |
The effectiveWin formula: for each base entry, add win + sum(featureAwards × featureEvPerSpin) plus, for every progression counter increment on this entry, 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.
computeOverallKpis()
Compute overall statistics: RTP, hit rate, volatility, and max win.
computeOverallKpis(entries: IKpiEntry[]): IOverallKpiscomputeMetaTagKpis()
Compute per-tag statistics: frequency, RTP contribution, average win, etc.
computeMetaTagKpis(entries: IKpiEntry[]): IMetaTagKpis[]computeWinDistribution()
Compute a bucketed win distribution (e.g. "0x", "0.001–1x", "1.001–5x", etc.).
computeWinDistribution(entries: IKpiEntry[]): IWinDistributioncomputeWinOdds()
Compute win odds at standard thresholds (e.g. "1 in 5 chance of winning 2x or more").
computeWinOdds(entries: IKpiEntry[]): IWinOddsclassifyVolatility()
Classify a standard deviation value into a volatility category.
classifyVolatility(stdDev: number): TVolatilityClass| Range | Classification |
|---|---|
| < 5 | 'Low' |
| 5–10 | 'Medium' |
| 10–15 | 'High' |
| >= 15 | 'Very High' |
generateBucketName()
Generate a human-readable label for a win bucket range.
generateBucketName(min: number, max: number): stringExamples: "0x", "0.001–1x", "100x+".
Types
IKpiEntry
Input type for all KPI functions. Matches the shape of loaded entries.
interface IKpiEntry {
win: number;
weight: number;
metaTags: string[];
featureAwards?: TFeaturesAwarded | null;
progressionAwards?: Record<string, number> | null;
}IAllKpis
Complete KPI result returned by computeAllKpis().
interface IAllKpis {
overall: IOverallKpis;
metaTags: IMetaTagKpis[];
winDistribution: IWinDistribution;
winOdds: IWinOdds;
gameRtp: IGameRtp | null;
}IOverallKpis
interface IOverallKpis {
totalEntries: number;
totalWeight: number;
rtp: number;
hitRate: number;
volatilityStdDev: number;
volatilityClass: TVolatilityClass;
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 | TVolatilityClass | 'Low' | 'Medium' | 'High' | 'Very High' |
maxWin | number | Highest win amount |
IGameRtp
interface IGameRtp {
baseRtp: number;
featureBreakdowns: IFeatureRtpBreakdown[];
totalRtp: number;
allFeaturesLoaded: boolean;
minTotalRtp?: number;
maxTotalRtp?: number;
}| Field | Type | Description |
|---|---|---|
baseRtp | number | Base game RTP percentage |
featureBreakdowns | IFeatureRtpBreakdown[] | Per-feature RTP contributions (sorted by contribution descending) |
totalRtp | number | Base + all feature 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. |
IFeatureRtpBreakdown
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. |
IMetaTagKpis
interface IMetaTagKpis {
tag: string;
entryCount: number;
totalWeight: number;
frequency: number;
rtp: number;
rtpContribution: number;
hitRate: number;
volatilityStdDev: number;
averageWin: number;
maxWin: number;
}IWinDistribution
interface IWinDistribution {
buckets: IWinBucket[];
}
interface IWinBucket {
label: string;
probability: number;
entryCount: number;
}IWinOdds
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) |
TVolatilityClass
type TVolatilityClass = 'Low' | 'Medium' | 'High' | 'Very High';IProgressionCounterConfig
Configuration for a progression counter that triggers feature spins on completion.
interface IProgressionCounterConfig {
name: string;
onComplete: {
type: string;
awards: { count: number; feature: string; weighting?: number }[];
};
}| 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 | Feature spins awarded on completion |