Maths File Conventions

How to author a new game's maths/*.json so that the certification tooling works on it without anyone having to write game-specific tool code.

Audience: anyone adding a new game to this workspace, or reviewing a maths file before it goes to a test lab. No prior knowledge of the tooling is assumed.

Table of Contents

  1. Background: what the maths file is used for
  2. Why naming matters so much
  3. File name, location and registration
  4. Top-level structure
  5. Section keys
  6. The config block
  7. Reel sets
  8. Weight table names
  9. Weight table shapes
  10. Sections the tooling ignores
  11. Patterns that cost extra work
  12. Documenting the tables for the test lab
  13. Worked example
  14. How to verify your file
  15. Checklist
  16. Glossary

1. Background: what the maths file is used for

A game's maths file is a single JSON document holding everything the game engine needs to produce a spin: the reel strips, the pay table, and every probability table the engine draws from with the random number generator.

Four consumers read it, and each imposes some requirement on how it is written:

ConsumerWhat it doesWhat it needs from the maths file
The game enginePlays spinsWhatever the game's own interfaces declare
simulateRuns millions of spins to measure RTPSection names, to know which bet types exist
rng-traceRecords every RNG call and labels it with the table it drew fromSection names, reel set names, weight table names and shapes, grid layout
generate-deliverablesAssembles the package sent to the test labThe file name, and that every table is documented

The demanding one is rng-trace, because its output is a certification document. It produces a report listing every random number the engine consumed, in order, annotated with which probability table each one came from. A test lab uses that to confirm the certified RNG really drives the game outcomes, and cross-checks it against the PAR sheet.


2. Why naming matters so much

The trace tool contains no knowledge of any game. It discovers a game's probability tables by reading the maths file: it looks for keys that are named like weight tables, sums each table's weights, and then labels an RNG call by matching the call's range against those totals.

This is a deliberate design choice. The alternative, a list of known table names inside the tool, would have to be edited every time a game is added, and editing shared tool code can silently change the output for a game that has already been signed off by a lab. Discovery by convention means adding a game touches only that game's own files.

The consequence for you as a maths author is simple:

A table the tool cannot recognise becomes a call the test lab sees with no explanation next to it.

Nothing crashes, and nothing is labeled wrongly. The information is just missing, and the lab will ask about it. Everything below is about making your tables recognisable.

A game that genuinely cannot follow these conventions is still supported: a per-game "tracer" file can be written to handle it. That is real work, and avoiding it is usually a matter of naming a key well, so prefer to design it out.


3. File name, location and registration

Path

games/<gameId>/src/maths/<mathFileName>-<MODE>.json

Rules

PartRequirementWhy
<mathFileName>Must equal the mathFileName field on the game's plugin classThe plugin loads maths with `${this.mathFileName}-${mode}.json`
<MODE>Uppercase letters followed by digits, such as R3 or R4The deliverables script extracts the example mode for the lab's README using /-([A-Z]+\d+)\.json$/. Lowercase or non-numeric names are not recognised
One file per modeEvery entry in the plugin's mathModes array needs its own fileonLoad() loads all declared modes at startup and fails if one is missing

The three plugin fields that tie together:

readonly gameId = 'mygame';              // used by tooling to find per-game files
readonly mathModes = ['R4'];             // one maths file per entry
readonly mathFileName = 'my-game';       // maths/my-game-R4.json

Note gameId is condensed with no separators, while mathFileName is usually hyphenated. That is expected; they serve different purposes.

Registration, easy to forget

A new game must be listed in its pack's games.json, for example apps/pack-alpha/games.json:

{ "games": ["...", "mygame"] }

The pack's webpack build reads this to externalise each game. The new-game generator adds the entry for you; if you create a game by hand and skip it, the build will not treat your game correctly.

The maths files themselves are copied into the build output by the game's own webpack.config.js, which copies src/maths to dist/games/<game>/maths after compiling. You do not need to configure that, but it is why the maths must live in src/maths and nowhere else.


4. Top-level structure

A maths file is one object whose top-level keys are of three kinds:

{
  "config":   { },            // layout and game constants     -> read by tooling
  "BG":       { },            // a game-mode section           -> read by tooling
  "FG":       { },            // a game-mode section           -> read by tooling
  "payTable": { },            // game data                     -> ignored by tooling
  "symbolsMap": { }           // game data                     -> ignored by tooling
}

Only the mode sections and config are interpreted. Everything else is the game's own business, and the tooling steps over it.

Do not add fields that the plugin computes at load time. For example, some games parse win line definitions during onLoad() and attach the result to the in-memory config. Those belong in code, not in the JSON.


5. Section keys

Section names are read literally. A misspelled section is invisible to the tooling, which means its tables are never labeled.

KeyMeaning
BGBase game spins
FGFree spins
ANTEBase spins when the ante bet is active
ANTE_FGFree spins reached from an ante round
BB_TriggerThe bought base spin in Buy Bonus mode (BBTrigger is also accepted, prefer the underscore form)
BBFree spins in Buy Bonus mode
FeatureTriggerCheckTables that decide whether a natural base spin triggers the feature, when they are kept outside BG
configLayout and constants, see below

Two points that are easy to get wrong:

Add ANTE_FG if ante free spins use different tables from ordinary free spins. Without that section, an ante round's free spins are labeled from the FG tables, which is incorrect whenever the two differ.

Only add ANTE or BB_Trigger if the game truly has that mode. When the section is absent, the tools refuse --ante or --buyBonus with a clear message. That is the desired behaviour: better an explicit refusal than a report labeled with a mode the game does not implement.

Sections are also how the tooling scopes labels. A table in BG and a table in FG may share a total weight without any confusion, because base spins and free spins are labeled from separate lookups. Only tables within the same scope can collide.


6. The config block

"config": {
  "layout": { "rows": 3, "columns": 5 }
}

config.layout.rows and config.layout.columns are the only config values the trace tool reads. Everything else in config (wild symbol, bet sizes, feature counts, symbol groupings) is for the game engine.

The layout must describe the real visible grid, because it is used to:

  • rebuild the symbol grid from the reel stop positions, and
  • recognise the uniform draws that pick a column and row when the engine places a symbol onto the grid.

If the visible grid can change size while a round is in progress, say so in review. The tool trusts this block, so spins at another size lose their placement labels. They are dropped rather than guessed, so nothing incorrect is printed.


7. Reel sets

Name every reel set with the prefix ReelSet_:

"ReelSet_1": [ ["L1","H2","L3", "..."], ["..."], ["..."], ["..."], ["..."] ]

The value is an array of reel strips, one per column, each strip an array of symbol names.

Prefer exactly one reel set per section. With one, the tool can rebuild the grid from the reel stops, print it next to the grid the engine reports, and use the difference between them to verify any symbol placement that happened afterwards. With two or more it cannot know which set a given spin used, so it skips both the rebuilt grid and the placement labels rather than print something that might be wrong.

If free spins select a reel set by symbol, name each one ReelSet_<symbol> using the exact symbol values that appear in the selection table. The report prints the round's set as ReelSet_<selected symbol>, so the names have to line up.


8. Weight table names

A key is treated as a probability table when its name matches:

/Weights?($|_)/

In words: the name contains Weight or Weights, followed either by the end of the name or an underscore.

NameRecognisedNote
WildCountWeightsyesthe usual form
STACK1_Weightsyesunderscore before the token is fine
Reel_Selection_Weightyessingular is fine
WildToAddWeights_4x5yesa suffix after the token is fine
ScatterChancesnono Weight token
CoinOddsnono Weight token
NumCoinsnono Weight token
WeightedPicksnotoken is not followed by end-of-name or _

The single most useful habit in this whole document: put Weight or Weights in the name of every key the engine draws from with the RNG.

It costs nothing and it is the difference between a game that needs no tool code and one that does.

The reverse also matters: keys that are not RNG tables should not carry the token, so plain lookup maps keep being ignored correctly.


9. Weight table shapes

Two shapes are recognised. They mirror the two SDK helpers that consume them, so use whichever matches how the engine reads the table.

Values and weights, consumed by getRandomValue:

"WildCountWeights": [[3, 4, 5], [1000, 10, 1]]

First array is the values, second array is their weights. Here 3 is drawn 1000 times out of 1011.

Name and weight pairs, consumed by selectWeightedRandom:

"FeatureTriggerWeights": [["Yes", 1], ["No", 189]]

Both are also recognised in two nested arrangements:

A dictionary of tables, keyed by game state. Each entry becomes its own label, written Name[key]:

"CoinsToAddWeights": {
  "6":  [[1, 2], [5, 1]],
  "10": [[2, 3], [4, 1]]
}

One level of grouping, for tables belonging to a named sub-feature:

"ScatterFeature": {
  "NumScattersWeights": [[3, 4], [9, 1]]
}

Nesting deeper than one level is not scanned. Keep weight tables either directly in a section or one group below it.


10. Sections the tooling ignores

These are normal and expected. The tooling does not read them, so name them however the game's interfaces require:

  • payTable, symbolsMap, winLines
  • multiplier and jackpot lookup maps
  • any other constant the engine needs

They are still shipped to the lab, because the whole maths file is hashed and included in the deliverable. So they must be correct, they simply carry no naming requirement from the tooling.


11. Patterns that cost extra work

None of these break anything. Each one means the game needs a hand-written tracer file to reach full labeling, so it is worth designing them out while the maths is still being written.

Two tables in the same section sharing a total weight

Because labels are resolved by matching the RNG range against a total, two tables in one scope with the same total cannot be told apart. Both names are printed, separated by |, and separating them properly requires a tracer that follows the order of calls.

This happens easily: a "how many symbols" table and a "final count" table often have the same shape and end up with the same total. It has occurred more than once in this workspace, including one game with four tables all totalling the same number.

If you can adjust a weight so the totals differ, do it. It removes the need for tracer code entirely, with no effect on game behaviour beyond the intended probabilities. The trace report ends with a LABEL SUMMARY block listing any tables that share a total, so this is quick to check.

Drawing from a modified copy of a table

Picking without replacement, by copying a table and removing entries as they are drawn, changes the total on every draw. Only the first call matches the declared table; later calls match nothing and stay unlabeled.

If the game needs this, expect to write a tracer for it.

A grid that changes size during a round

When the live row or column count differs from config.layout, symbol placement picks cannot be verified against the grid and are left unlabeled for those spins.


12. Documenting the tables for the test lab

Every weight table must be explained in the game's own README content file:

tools/cert/gamewise_v2/readme/games/<gameId>.md

This file supplies the per-game sections of the document shipped alongside the trace tool: a table of every weight table with its meaning, plus any game-specific notes and guidance on how many spins are needed for full coverage.

generate-deliverables cross-checks it against the maths and warns by name for anything undocumented:

WARNING: 1 weight table(s) appear in the maths but are not documented in
readme/games/<gameId>.md:
  InitialNaturalScatterWeights
  The trace will label these calls, with no explanation for the agency.

The trace labels a table whether or not anyone documented it, so an undocumented table reaches the lab as an unexplained label. Treat that warning as a blocker for delivery.


13. Worked example

A minimal file that is fully conventional:

{
  "config": {
    "layout": { "rows": 3, "columns": 5 },
    "wildSymbol": "WD",
    "baseBet": 9
  },

  "BG": {
    "ReelSet_1": [["L1","H2","..."], ["..."], ["..."], ["..."], ["..."]],
    "WildCountWeights": [[0, 1, 2], [20, 4, 1]]
  },

  "FeatureTriggerCheck": {
    "FeatureTriggerWeights": [["Yes", 1], ["No", 189]],
    "FeatureTypeWeights": [["Normal", 1], ["Special", 1]]
  },

  "FG": {
    "ReelSet_L1": [["..."]],
    "ReelSet_H1": [["..."]],
    "SymbolSelectionWeights": [["L1", 1], ["H1", 1]]
  },

  "payTable": { "H1": [0, 0, 50, 200, 1000] },
  "symbolsMap": { "H1": 1 }
}

Why this needs no tool code:

  • sections use the exact expected names
  • config.layout describes the real grid
  • BG has a single reel set, so grids can be rebuilt and placements verified
  • every RNG table carries the Weights token
  • all tables use a supported shape, none nested more than one level
  • the FG reel set names match the values in SymbolSelectionWeights
  • no two tables in the same scope share a total (base scope totals are 25, 190 and 2, all distinct)

14. How to verify your file

Run a trace and read the output:

pnpm nx rng-trace <gameId> --iterations=2000 --outputPath=temp/<gameId>-trace.txt

Then check three things:

1. Are there unlabeled calls under POST-REEL LOGIC? A call with no [...] annotation means the tool could not find its table. Usually a name missing the Weight token, or a table nested too deeply.

2. Does the closing LABEL SUMMARY report a non-zero ambiguous count? That means two tables share a total and both names were printed for the same call. Adjust a weight, or accept that the game needs a tracer.

3. Does the grid make sense? If you expected to see the reel-stop grid alongside the final grid and only one appears, the section probably has more than one reel set, or the layout does not match the real grid.

Also run the other two tools once, to confirm the file loads cleanly end to end:

pnpm nx simulate <gameId> --iterations=10000
pnpm nx generate-deliverables <gameId>

The second one prints a warning naming any weight table you have not documented.


15. Checklist

Naming and location

  • File at games/<gameId>/src/maths/<mathFileName>-<MODE>.json
  • <mathFileName> matches the plugin's mathFileName
  • <MODE> is uppercase letters followed by digits
  • One file per entry in the plugin's mathModes
  • Game listed in its pack's games.json

Structure

  • Section keys spelled exactly: BG, FG, ANTE, ANTE_FG, BB_Trigger, BB, FeatureTriggerCheck
  • ANTE_FG present if ante free spins differ from ordinary free spins
  • ANTE and BB_Trigger present only if the game really has those modes
  • config.layout.rows and columns match the real visible grid
  • No fields that the plugin computes at load time

Reels

  • Every reel set prefixed ReelSet_
  • Ideally one reel set per section
  • Symbol-selected reel set names match the values in the selection table

Weight tables

  • Every RNG-drawn table has Weight or Weights in its name
  • Every table uses one of the two supported shapes
  • Nothing nested more than one level below its section
  • No two tables in the same section share a total weight

Delivery

  • Every weight table documented in tools/cert/gamewise_v2/readme/games/<gameId>.md
  • A trace run shows no unlabeled post-reel calls and a clean LABEL SUMMARY

16. Glossary

TermMeaning
Weight tableA probability table the engine draws from with the RNG. Each entry has a weight; the chance of an entry is its weight divided by the total
Total weightThe sum of a table's weights. The RNG is called as RNG(0, total), which is how the trace tool identifies which table a call used
Reel stripThe full ordered list of symbols on one reel. Much longer than the visible grid
Reel stopThe 0-indexed position on a strip where a spin came to rest. The visible symbols are the ones from that position onwards, wrapping at the end
ScopeWhether a call happened during a base spin or a feature spin. Labels are looked up per scope, so tables in different scopes cannot be confused
SectionA top-level block of the maths file for one game mode, such as BG or FG
TracerAn optional per-game file in the tooling that handles anything the conventions cannot cover. Adding one never affects another game
PAR sheetThe document given to a test lab describing the game's probabilities and expected return. The trace report is cross-checked against it
Test lab / agencyThe external body that certifies the game, for example eCOGRA
Built with LogoFlowershow