Frosted Market Bonanza — XLSX to Runtime Math Guide
Frosted Market Bonanza — XLSX to Runtime Math Guide
This document is based on the actual repository code, not on assumptions about slot math. It traces the path from the mathematician’s workbook to the runtime game JSON and then to the game logic that produces a spin result.
1. Executive summary
The real flow in this repo is:
Math team workbook ↓ raw XLSX in feg_parse-maths/rawMaths/frostedMarketBonanza ↓ parser in feg_parse-maths/src/commands/frosted-market-bonanza.commands.ts ↓ JSON output in feg_game_be/games/frostedmarketbonanza/src/maths/frosted-market-bonanza-R3.json ↓ plugin in feg_game_be/games/frostedmarketbonanza/src/plugin.ts ↓ engine in feg_game_be/games/frostedmarketbonanza/src/engine.ts ↓ logic in feg_game_be/games/frostedmarketbonanza/src/logic.ts ↓ response DTO in feg_game_be/games/frostedmarketbonanza/src/interfaces.ts
The key point is that this is not a generic Excel-to-engine pipeline. The parser reads very specific ranges from specific sheets and writes a game-specific JSON object. The game runtime loads that JSON and does not read the original XLSX at all.
2. Inventory of the raw math folder
The raw mathematician files are under:
Files found
| File | Extension | Meaning in repo | Notes |
|---|---|---|---|
frosted_market_bonanza_96.5.xlsx | .xlsx | Raw source workbook | Likely a prior version |
frosted_market_bonanza_v2_96.5.xlsx | .xlsx | Active source workbook used by parser | Parser explicitly reads this file |
~$frosted_market_bonanza_96.5.xlsx | temporary Excel lock file | Temporary file created by Excel | Ignore |
~$frosted_market_bonanza_v2_96.5.xlsx | temporary Excel lock file | Temporary file created by Excel | Ignore |
Which files look like source math files?
The two .xlsx files are the source math workbooks. The parser specifically reads:
The key line is:
const workbook = readMathFile('./rawMaths/frostedMarketBonanza/frosted_market_bonanza_v2_96.5.xlsx');
So the actual source workbook used by this repo is the v2 workbook.
Workbook sheet inventory
The workbook is a 7-sheet workbook. The actual sheet names are:
ResultBase GameFree Game featureAnteAnte Free Game featureBuy Bonus featureBaB Trigger
This was confirmed by reading the raw workbook XML directly from the .xlsx file; the parser then uses only selected sheets and ranges.
Important workbook summary
The Result sheet contains summary math values like RTP and hit-rate metrics.
Examples from the workbook:
- Total Game RTP:
0.96475316 - Base Game RTP:
0.62057222199999995 - Free Games RTP:
0.344191674 - Buy Bonus RTP:
0.93211143775000005
These are summary values in the workbook, but they are not the values the parser reads as part of the runtime config. The parser reads specific ranges on the feature sheets, not the Result summary sheet.
3. The actual Excel workbook and its meaning
The parser reads the workbook in a very direct, range-based way. It does not infer the layout generically.
The workbook structure matches a slot game with multiple modes:
Base GameAnteFree Game featureAnte Free Game featureBuy Bonus featureBaB Trigger
The parser code is explicit about this:
worksheet = workbook.Sheets[workbook.SheetNames[1]]; // Base Game
worksheet = workbook.Sheets[workbook.SheetNames[2]]; // Free Game feature
worksheet = workbook.Sheets[workbook.SheetNames[5]]; // Buy Bonus feature
worksheet = workbook.Sheets[workbook.SheetNames[6]]; // BaB Trigger
worksheet = workbook.Sheets[workbook.SheetNames[3]]; // Ante
worksheet = workbook.Sheets[workbook.SheetNames[4]]; // Ante Free Game feature
This is a strong repository signal that the workbook is specifically designed for the same game across multiple modes, not a single universal template.
The sheet-by-sheet purpose as seen in the code
| Sheet | Parser use | Meaning from workbook text | Code evidence |
|---|---|---|---|
Result | Not parsed for runtime math | Summary RTP / hit-rate sheet | The parser ignores this sheet |
Base Game | Yes | Base game, normal reel set, stack weights, pay table, nudge, symbol conversion | BG_* ranges in parser |
Free Game feature | Yes | Free-spin reel sets, bomb multiplier weights, stack weights | FG_* ranges |
Ante | Yes | Ante variant, same base-game mechanics with altered bet and scatter treatment | ANTE_* ranges |
Ante Free Game feature | Yes | Ante free-spin variant | ANTE_FG_* ranges |
Buy Bonus feature | Yes | Bonus trigger / buy-bonus mode | BB_* ranges |
BaB Trigger | Yes | Buy-bonus trigger reel set and scatter weights | BB_Trigger_* ranges |
Important sheet-level facts from the workbook XML
From the workbook metadata and early rows:
Base Gamesheet says rows =5, reels =6, base bet =20, total bet =20Antesheet says rows =5, reels =6, base bet =25, total bet =25Free Game featuresheet says rows =5, reels =6, base bet =1, cost to cover =2000- This matches the runtime config in feg_game_be/games/frostedmarketbonanza/src/maths/frosted-market-bonanza-R3.json, where the game config sets
layout.rows = 5,layout.columns = 6andbaseBet = 20,anteBaseBet = 25,bbBaseBet = 20.
What cannot be determined confidently?
The workbook names like RallyBracketsWeights, NonRallyBracketsWeights, and RallyNonRallyCascadeWeights are clearly used in code and not purely cosmetic, but the exact mathematical semantics of “Rally / Brackets / Cascade” cannot be confidently mapped from the workbook alone without domain-specific rule docs. The code treats them as weighted arrays used in the runtime logic via selectWeightedRandom and cascade refill helpers, but the business meaning is not obvious from the workbook alone.
This is an example of a place where the repository is clear about the data flow but not the original math-team intent.
4. Mathematical model used by the game
This game is not a standard paylines game. The runtime implementation clearly uses a cluster/cascade model.
Confirmed in code
The logic in feg_game_be/games/frostedmarketbonanza/src/logic.ts does the following:
const minClusterSize = 8;
const allBracketSymbols = ['H1', 'H2', 'H3', 'H4', 'L1', 'L2', 'L3', 'L4', 'L5'];
It then groups symbols and builds clusters with:
const symbolCounts = groupSymbolPositions({ reelView: currentReelView });
...
if (count >= minClusterSize) {
clusters.push({ symbol, positions, size: count });
}
This is a cluster game. The win is based on matching symbol clusters of at least 8 positions, not a payline pattern.
Important mechanics actually present
The code and workbook confirm these features are active:
- Cluster wins: yes
- Cascade: yes
- Free spins: yes
- Scatter trigger: yes
- Wilds: no generic wild symbol in the runtime config; the symbol map includes only
STACK1,STACK2,SC, and ranked symbols - Multiplier bombs in free spins: yes
- Nudge feature: yes
- Symbol conversion feature: yes
- Buy bonus: yes
- Ante game: yes
The repository also documents a known issue in feg_game_be/docs/superpowers/handoff/2026-09-21-frosted-market-bonanza-freespin-trigger-bug.md, which confirms the game logic is not purely theoretical and that feature triggers can be silently dropped under conversion conditions.
How the math works in simple terms
The game begins with a reel board. Then:
- The base game chooses a reel set and generates a board.
- The game replaces stack symbols according to weight tables.
- It groups adjacent matching symbols into clusters.
- It pays on clusters with size >= 8.
- Winning symbols are removed and replaced by cascading symbols.
- It checks for scatter count; if the threshold is reached, it triggers free spins.
- In base game, optional nudge and symbol conversion can alter the final board if the conditions are met.
- In free spins, bomb multipliers are assigned and applied to the free-spin total.
5. Symbol information
The parser defines the symbol map at the top of feg_parse-maths/src/commands/frosted-market-bonanza.commands.ts:
private symbolsMap = Object.freeze({
Pic1: 'H1',
Pic2: 'H2',
Pic3: 'H3',
Pic4: 'H4',
A: 'L1',
K: 'L2',
Q: 'L3',
J: 'L4',
'10': 'L5',
BOMB: 'BN',
Scatter: 'SC',
STACK1: 'STACK1',
STACK2: 'STACK2',
});
This is extremely important. The Excel layer uses labels like Pic1, Pic2, A, K, Q, J, 10, Scatter, STACK1, and STACK2. The runtime JSON uses canonical IDs such as H1, H2, H3, H4, L1, L2, L3, L4, L5, SC, BN, and STACK1 / STACK2.
Symbol classes in the repo
| Excel label | Runtime ID | Role |
|---|---|---|
Pic1 | H1 | high-paying symbol |
Pic2 | H2 | high-paying symbol |
Pic3 | H3 | high-paying symbol |
Pic4 | H4 | high-paying symbol |
A | L1 | low-paying symbol |
K | L2 | low-paying symbol |
Q | L3 | low-paying symbol |
J | L4 | low-paying symbol |
10 | L5 | low-paying symbol |
Scatter | SC | scatter trigger |
BOMB | BN | multiplier bomb in free games |
STACK1 | STACK1 | stack symbol |
STACK2 | STACK2 | stack symbol |
Where this matters in runtime
The JSON config stores symbolsMap and the logic uses it to convert worksheet labels into final runtime symbol IDs. The actual logic checks for:
configSymbols.scatter->SCconfigSymbols.multiplierSymbol->BN
This is defined in the JSON config at the bottom of feg_game_be/games/frostedmarketbonanza/src/maths/frosted-market-bonanza-R3.json:
"symbols": {
"scatter": "SC",
"multiplierSymbol": "BN"
}
What about a generic wild symbol?
The code does not define a standard wild symbol for the game. Symbol replacement and cluster logic revolve around ranked symbols, stack symbols, scatter, and bomb multipliers. That is consistent with the repository code.
6. Reel / board information
The board is a 5x6 grid. This is confirmed in multiple places:
- parser
layout = '5x6' - workbook
Base Gamesheet statesRows = 5,Reels = 6 - runtime config in JSON says
layout.rows = 5,layout.columns = 6
The parser reads reels using helpers from feg_parse-maths/src/helpers/index.ts:
export const extractReels = (worksheet, startRange, numReels) => { ... }
The helper reads a selected range, extracts cells, filters nulls, and returns arrays of symbol tokens per reel. It then maps each raw symbol through this.symbolsMap.
Example
The parser extracts base game reel values from ranges such as:
BG_REEL1_RANGE: 'H15:H118'
and then reuses the same strategy across columns using extractReels(...) and getNextAlphabet(...) to move across reel columns.
This means the Excel workbook stores reel strips in a columnar format, and the parser converts them to arrays like:
ReelSet_1: [ ["H2","H2","L2",...], ["L2","L2","L1",...], ... ]
The runtime uses generateReelView(...) from the slot platform SDK to build the actual two-dimensional board from those reel strips.
Board interpretation
The board is effectively a 6-column by 5-row matrix. The code comments say:
- select a reelstop from the selected reelset
- create a 6x6 interface
- display the bottom 5 rows of the selected 6x6 interface
This is consistent with a 6-column board where only the bottom 5 rows are visible and the top row is a hidden/selection context. The runtime code uses the board as a 5-row visible board, but the math workbook refers to a 6x6 internal interface.
7. Weights and the probability question
This is one of the most important parts.
The Excel weight values
The parser reads weight tables such as:
BG_STACK1_WEIGHTS_RANGE: 'S6:T9'BG_STACK2_WEIGHTS_RANGE: 'V6:W10'FG_BOMB_SET1_Weights_RANGE: 'AC21:AD33'BB_TRIGGER_SC_Weights_RANGE: 'I23:J25'NudgeFeature.TriggerWeightsandNumReelsWeights
These values are eventually stored as arrays of [value, weight] pairs.
How they are read
The parser does:
mathJson.BG.STACK1_Weights = mapStackSymbol(
getRangeData(worksheet, this.excelRangeObj.BG_STACK1_WEIGHTS_RANGE, false, false),
this.symbolsMap,
);
The rest of the runtime uses them with functions like:
selectWeightedRandom(mathByGameType.NudgeFeature.TriggerWeights)
and
selectWeightedRandom(bombWeights!)
This confirms that the Excel weight is not a direct probability value in the math engine. It is a weighted random selection input. The runtime chooses a bucket according to the weight values, not by treating the raw numbers as direct probabilities.
Important distinction
Weight does not automatically equal probability in a single mathematical sense unless the author of the weight table explicitly normalizes it. The code treats the value as weight, and the random selector interprets it as weighted choice probability.
Chain of a weight
Excel weight table
↓
Parser reads range and stores [symbol, weight]
↓
selectWeightedRandom chooses among values
↓
Game logic decides a feature or symbol replacement
↓
Board outcome changes
Concrete example
The base game symbol-conversion feature uses two weight tables:
SymbolConversionFeature.LowSymbolWeightsSymbolConversionFeature.HighSymbolWeights
These produce a selected pair like selectedLow and selectedHigh, and then the code rolls TriggerWeights to decide if the conversion occurs.
This is implemented in feg_game_be/games/frostedmarketbonanza/src/symbol-conversion.ts.
8. Paytable and payout logic
The parser builds the paytable from the BG_PAY_TABLE_RANGE:
BG_PAY_TABLE_RANGE: 'B12:E20'
Then it calls:
mathJson.payTable = this.extractPayTable(worksheet, this.excelRangeObj.BG_PAY_TABLE_RANGE);
and later adds scatter pay data from:
BG_SC_PAY_RANGE: 'C23:E24'
How the paytable is transformed
The parser’s extractPayTable() method does this:
- Takes the raw rows from the Excel table
- Takes the first cell as the symbol identifier
- Maps it through
this.symbolsMap - Builds a compact array of length 12 for cluster sizes 1..12
- Fills payout values for 8+, 9+, 10+, 11+, 12+ cluster sizes
if (cleanPayouts[0]) payArray[11] = cleanPayouts[0]; // 12+ symbols
if (cleanPayouts[1]) {
payArray[9] = cleanPayouts[1]; // 10 symbols
payArray[10] = cleanPayouts[1]; // 11 symbols
}
if (cleanPayouts[2]) {
payArray[7] = cleanPayouts[2]; // 8 symbols
payArray[8] = cleanPayouts[2]; // 9 symbols
}
That is the concrete translation from spreadsheet payout cells into the runtime paytable.
Win calculation in logic
In feg_game_be/games/frostedmarketbonanza/src/logic.ts:
const symbolPayouts = math.payTable[cluster.symbol];
const payIndex = Math.min(cluster.size - 1, (symbolPayouts?.length ?? 0) - 1);
const symbolPay = symbolPayouts?.[payIndex] ?? 0;
const winAmount = multiply(symbolPay, stakeValue) ?? 0;
This means:
- find the cluster size
- get the payout for that symbol at that cluster size
- multiply by stake value
- add the win to cascade total
Scatter pay
Scatter pay is applied after the cluster evaluation when the scatter count has reached the trigger threshold:
if (lastScCount >= fsTriggerCount) {
const scPayouts = math.payTable[configSymbols.scatter];
scatterPay = multiply(scPayouts?.[lastScCount - 1] ?? 0, stakeValue);
}
The final totalWin is cluster wins plus scatter pay.
9. RTP, hit rate, volatility, and distribution
The workbook’s Result sheet has summary statistics such as:
- Total Game RTP:
0.96475316 - Base Game RTP:
0.62057222199999995 - Free Games RTP:
0.344191674 - Buy Bonus RTP:
0.93211143775000005
The repo also tracks feature distributions in the simulation analyzer at feg_game_be/tools/game-tools/src/executors/simulate/analyzers/games/frosted-market-bonanza.analyzer.ts.
This analyzer tracks:
- cluster wins
- scatter pay wins
- free-game wins
- bomb multipliers
- nudge triggers
- symbol conversion triggers
- win distribution
- reel-stop frequencies
Are these values used at runtime?
The answer is mostly no, not directly.
- The parser does not read the
Resultsheet for runtime game behavior. - The runtime JSON config contains game operational data, not the summary statistical workbook.
- The simulation analyzer is for statistics and testing, not the live engine path.
So the RTP and hit-rate values appear to be:
- math-team summary values
- validation data
- simulation output
- not direct runtime inputs for the live slot engine
This distinction matters. They are not the same as the weights and reel-set arrays that the runtime actually consumes.
10. Parser flow in the repo
The real parser is in feg_parse-maths/src/commands/frosted-market-bonanza.commands.ts.
Parser entry point
The command is registered in feg_parse-maths/src/cli.module.ts and the CLI entry point is in feg_parse-maths/src/main.ts.
How the parser actually works
- Open the workbook with
XLSX.readFile(...) - Select a sheet using
workbook.Sheets[workbook.SheetNames[n]] - Use
extractReels()to read the reel strip columns - Use
getRangeData()to read weight tables, cascade tables, and pay tables - Apply
this.symbolsMapto map spreadsheet labels to canonical runtime identifiers - Write the final JSON object to
./output/frosted-market-bonanza-R3.json
The final write is:
writeFileSync(this.outputJsonFile, JSON.stringify(mathJson, null, 2), 'utf8');
Important parser helper functions
From feg_parse-maths/src/helpers/index.ts:
readMathFile(filePath)→ reads workbookgetRangeData()→ reads an Excel range into plain JS dataextractReels()→ extracts reel strips from a rangemapStackSymbol()→ remaps stack symbol labels usingsymbolsMap
This is the real conversion layer from notebook-style spreadsheet to runtime JSON.
11. Excel → code mapping
This is the most practical mapping for a new developer.
| Excel location | Parser usage | Runtime property | Used by |
|---|---|---|---|
Base Game sheet, H15:H118 | extractReels() | BG.ReelSet_1 | base game board generation |
Base Game sheet, S6:T9 | mapStackSymbol(getRangeData(...)) | BG.STACK1_Weights | stack replacement |
Base Game sheet, V6:W10 | mapStackSymbol(getRangeData(...)) | BG.STACK2_Weights | stack replacement |
Base Game sheet, B12:E20 | extractPayTable() | payTable | cluster wins and scatter pay |
Base Game sheet, C23:E24 | getRangeData() | payTable.SC | scatter payout |
Base Game sheet, AN14:AO16 | getRangeData() | BG.NudgeFeature.TriggerWeights | nudge trigger decision |
Base Game sheet, AQ14:AR16 | getRangeData() | BG.NudgeFeature.NumReelsWeights | nudge reel count |
Base Game sheet, AU15:AV16 | getRangeData() | BG.SymbolConversionFeature.TriggerWeights | conversion trigger |
Base Game sheet, AT20:AU23 | mapStackSymbol(getRangeData(...)) | BG.SymbolConversionFeature.HighSymbolWeights | conversion target |
Base Game sheet, AW20:AX24 | mapStackSymbol(getRangeData(...)) | BG.SymbolConversionFeature.LowSymbolWeights | conversion source |
Free Game feature sheet, AC6:AD7 | getRangeData() | FG.ReelSelectionWeights | free-spin reel set selection |
Free Game feature sheet, AC21:AD33 | getRangeData() | FG.BOMB_SET1_Weights | bomb multiplier values |
Free Game feature sheet, AF21:AG33 | getRangeData() | FG.BOMB_SET2_Weights | bomb multiplier values |
BaB Trigger sheet, B4:B39 | extractReels() | BB_Trigger.ReelSet_1 | buy-bonus trigger board |
BaB Trigger sheet, I23:J25 | getRangeData() | BB_Trigger.SC_Weights | scatter weight for buy bonus |
This is the practical mapping a developer can use when someone says “I see a value in the workbook” and wants to know where it lands in the code.
12. Final parsed math object structure
The final shape created by the parser is in feg_game_be/games/frostedmarketbonanza/src/interfaces.ts. The root math config includes this structure:
export interface FrostedMarketBonanzaMathConfig {
BG: BaseGameMathConfig;
FG: FreeGameMathConfig;
BB: FreeGameMathConfig;
ANTE: BaseGameMathConfig;
ANTE_FG: FreeGameMathConfig;
BB_Trigger: BbTriggerMathConfig;
payTable: Record<string, number[]>;
maxWin: number;
symbolsMap: Record<string, string>;
config: {
layout: { rows: number; columns: number };
symbols: { scatter: string; multiplierSymbol: string };
features: {
freeSpinsTrigger: { minScatters: number; initialCount: number };
freeSpinsRetrigger: { minScatters: number; additionalSpins: number };
};
maxWinMultiplier: number;
maxWinBbMultiplier: number;
baseBet: number;
bbBaseBet: number;
anteBaseBet: number;
};
}
The JSON file mirrors this structure and is the runtime source of truth.
Important property explanations
| Property | Type | Meaning | Source |
|---|---|---|---|
BG | object | Base-game math block | workbook Base Game |
FG | object | Free-game math block | workbook Free Game feature |
BB_Trigger | object | Buy-bonus trigger mode | workbook BaB Trigger |
ANTE | object | Ante-mode math | workbook Ante |
payTable | Record<string, number[]> | Cluster payout table | parser pay table range |
symbolsMap | map | Raw spreadsheet labels → runtime IDs | parser symbolsMap |
config.layout | object | rows and columns | workbook and runtime config |
config.features | object | Scatter trigger counts | JSON config |
maxWinMultiplier | number | Max win config cap | runtime config |
13. Where the parsed data goes after parsing
The actual path is:
Excel workbook
↓
parser writes frosted-market-bonanza-R3.json
↓
FrostedMarketBonanzaPlugin.onLoad() reads the JSON
↓
plugin.getMath(mode) loads the selected mode
↓
engine.executeSpin(...) or engine.executeFeature(...) uses the config
↓
logic.spin(...) / logic.runFeature(...) applies the rules
↓
SpinResult is returned to the platform
Concrete file path chain
- parser writes output: feg_game_be/games/frostedmarketbonanza/src/maths/frosted-market-bonanza-R3.json
- plugin loads this file: feg_game_be/games/frostedmarketbonanza/src/plugin.ts
- engine orchestrates: feg_game_be/games/frostedmarketbonanza/src/engine.ts
- logic decides winners: feg_game_be/games/frostedmarketbonanza/src/logic.ts
- responses are defined in feg_game_be/games/frostedmarketbonanza/src/interfaces.ts
14. Request → response connection
The runtime game input is defined at the top of the interfaces file.
Request fields
The GameSpinInput includes:
betAmountante?: booleanbuyBonus?: booleancombination?: number[]devMode?: booleanmaxWin?: numberforceNudgeFeature?: stringforceNudgeReels?: numberforceSymbolConversion?: booleanforceBonusMultiplier?: number
The plugin takes the API input and builds a game-specific view:
const spinInput: GameSpinInput = {
betAmount,
ante,
buyBonus: isBuyBonus,
combination,
devMode,
maxWin: rgsMaxWin,
forceNudgeFeature,
forceNudgeReels,
forceSymbolConversion,
forceBonusMultiplier,
};
Response fields
The actual game result object is SpinResult in feg_game_be/games/frostedmarketbonanza/src/interfaces.ts. It includes:
cascadeDatabettotalWinbaseWinnextFeaturependingFeaturefeatureResultsmultiplierlineWinscatterPayreelModifiers
This is what the platform ultimately returns to a caller.
15. Full spin flow example
The best logical example is a normal base spin.
Step 1: request enters the plugin
The game entry point is feg_game_be/games/frostedmarketbonanza/src/plugin.ts:
const result = this.engine.executeSpin(spinInput, math);
Step 2: engine chooses the correct mode
In feg_game_be/games/frostedmarketbonanza/src/engine.ts:
const gameType = buyBonus ? 'BB_Trigger' : ante ? 'ANTE' : 'BG';
Then it picks the relevant reel strips.
Step 3: reel view generation
const { reelView, reelStops: capturedReelStops } = generateReelView({
rows: layout.rows,
columns: layout.columns,
reelStrips,
cheatCombination: devMode && combination ? combination : undefined,
});
Step 4: logic evaluates clusters and scatter triggers
The real game outcome is in feg_game_be/games/frostedmarketbonanza/src/logic.ts:
- determine
gameType - load
mathByGameType - replace stack symbols
- group symbols and find clusters
- evaluate cluster payouts
- apply scatter triggers
- maybe trigger nudge or symbol conversion
- maybe trigger free spins
Step 5: feature evaluation
If the scatter count reaches the threshold, the logic sets:
spinResult.nextFeature = 'freespins';
spinResult.pendingFeature = features.freeSpinsTrigger.initialCount;
Then the game enters runFeature(), which selects a free game reel set and runs additional spins.
Step 6: response
The plugin returns:
return {
totalWin: result.totalWin,
featureTriggered: result.nextFeature != null,
gameData: result,
baseWinAmount: result.totalWin,
bonusWinAmount: 0,
bonusRoundCount: 0,
};
16. One Excel value through to the game result
A good example is the scatter trigger threshold from the workbook and runtime config.
Excel value
In the workbook config, the free-spin trigger is effectively controlled by scatter count thresholds and feature weights. The runtime JSON declares:
"freeSpinsTrigger": {
"minScatters": 4,
"initialCount": 10
},
"freeSpinsRetrigger": {
"minScatters": 3,
"additionalSpins": 5
}
Path through the system
- Excel workbook defines scatter logic.
- Parser reads the relevant tables and stores the config values in the JSON.
- Runtime plugin loads the JSON.
- Logic checks:
if (lastScCount >= fsTriggerCount) {
spinResult.nextFeature = 'freespins';
}
- The result populates
nextFeatureandfeatureResults. - The platform returns
featureTriggered: trueand the game state is persisted ingameData.
This is a good example of how a number in the spreadsheet becomes a real gameplay decision in the backend.
17. Important math functions
FrostedMarketBonanzaLogic.spin()
File: feg_game_be/games/frostedmarketbonanza/src/logic.ts
Purpose: main spin resolver for base game and free spins.
Input: total bet, math config, reel view, game mode flags, max win, optional forced feature inputs.
Processing:
- chooses game type
- replaces stack symbols
- groups symbol positions
- calculates cluster wins
- handles cascade refill
- checks scatter trigger
- may trigger nudge / conversion / free spins
- applies win cap
Output: SpinResult object including wins, next feature, modifiers, and state.
FrostedMarketBonanzaEngine.executeSpin()
File: feg_game_be/games/frostedmarketbonanza/src/engine.ts
Purpose: build the reel board and orchestrate a base spin.
Important behavior:
- selects reel strips by game type
- generates reel view using
generateReelView() - runs
logic.spin() - if symbol conversion is triggered, reruns the spin on converted board
applySymbolConversion()
File: feg_game_be/games/frostedmarketbonanza/src/symbol-conversion.ts
Purpose: passive conversion from selected low symbol to selected high symbol.
Important behavior:
- chooses low and high symbols
- checks if enough symbols are present
- uses trigger weight table
- replaces cells and records changed positions
FrostedMarketBonanzaLogic.runFeature()
File: feg_game_be/games/frostedmarketbonanza/src/logic.ts
Purpose: resolves the free-spin feature continuation.
Important behavior:
- selects free-spin reel set
- runs
spinFreeGame() - applies bomb multipliers
- updates feature totals and pending spins
- closes feature when counters drain or win cap is hit
18. Features actually present
Scatter free spins
- Trigger: at least
minScatterson a board - Implementation:
lastScCount >= fsTriggerCount - Relevant config:
freeSpinsTriggerin the JSON - Runtime logic:
spinResult.nextFeature = 'freespins'
Nudge feature
- Trigger: non-winning base spin with exactly 3 scatters and a weighted nudge decision
- Relevant files:
logic.ts,BG.NudgeFeature,ANTE.NudgeFeature - Runtime effect: modifies a few reels and optionally adds a scatter
Symbol conversion feature
- Trigger: non-winning base spin where line win = 0 and nudge did not fire
- Relevant files:
symbol-conversion.ts,engine.ts,logic.ts - Runtime effect: converts selected low symbols into high symbols and sometimes creates a win or a feature path
Bomb multipliers in free games
- Trigger: bomb (
BN) symbol appears in free spins - Relevant config:
BOMB_SET1_Weights,BOMB_SET2_Weights - Runtime effect: multiplier is applied to totals and pushed onto the cascade data
Buy bonus / BB trigger
- Trigger: buy bonus mode uses
BB_Triggerreel set - Relevant logic:
gameType = 'BB_Trigger' - Runtime effect: scatter placement and buy-bonus trigger path
19. Tests and validation evidence
There are no dedicated unit tests for Frosted Market Bonanza in the repo that directly assert specific spin outcomes. However, the repo does contain validation and simulation tooling:
- feg_game_be/tools/game-tools/src/executors/simulate/analyzers/games/frosted-market-bonanza.analyzer.ts
- feg_game_be/docs/superpowers/handoff/2026-09-21-frosted-market-bonanza-freespin-trigger-bug.md
These files are extremely valuable because they tell us:
- the analyzer tracks cluster wins and scatter payouts,
- the analyzer tracks bomb multipliers and nudge behavior,
- the repo has a known free-spin trigger bug after symbol conversion,
- the actual code behavior is more nuanced than the spreadsheet labels alone suggest.
20. Debugging guide for a new developer
If you need to follow one spin through the engine, the best breakpoints are:
- feg_game_be/games/frostedmarketbonanza/src/plugin.ts — start at
spin() - feg_game_be/games/frostedmarketbonanza/src/engine.ts — inspect
executeSpin() - feg_game_be/games/frostedmarketbonanza/src/logic.ts — inspect
spin() groupSymbolPositions()andremoveWinningSymbols()— see how clusters are built and removedselectWeightedRandom()— inspect the feature weights used to decide floats/transformationsrunFeature()— inspect the free-spin continuation state
Key fields to inspect while debugging:
reelViewspinResult.cascadeDataspinResult.nextFeaturespinResult.featureResultsspinResult.scatterPayspinResult.totalWinspinResult.reelModifierspendingFeaturelineWinmultiplier
21. Questions for the math team / unknowns
Some things are clear in code but not fully interpretable without the math team:
1. Meaning of RallyBracketsWeights and NonRallyBracketsWeights
- Found in workbook ranges and parser JSON fields
- The code passes them through to the runtime but does not explain their business meaning
- Ask: “What specific symbol pattern or bracket logic do these weights represent?”
2. Meaning of RallyNonRallyCascadeWeights
- The code parses them but does not show a direct business meaning in the game logic
- Ask: “What exact cascade path does each weight bucket represent?”
3. Symbol conversion trigger exact business rule
- The code implements the actual logic, but the workbook wording does not clearly explain the intended rule beyond the general logic
- Ask: “Is the conversion meant to be a pure passive feature or a recovery mechanic?”
4. Nudge rule semantics
- The code clearly implements withScatter/withoutScatter behavior, but the dataset does not tell us the original intended business condition in plain English
- Ask: “What exact board situation should trigger the nudge, and when is it intentionally suppressed?”
22. Final cheat sheet
Frosted Market Bonanza — Quick Reference
- Game Type: cluster/cascade slot with scatter-triggered free spins
- Reels: 6
- Rows: 5 visible board
- Win System: cluster-based, minimum cluster size 8
- Symbols:
H1..H4,L1..L5,SC,BN,STACK1,STACK2 - Wild: no standard wild symbol in the runtime model
- Scatter:
SC - Features: nudge, symbol conversion, free spins, bomb multipliers, buy bonus, ante mode
- Free Spins: yes, triggered by minimum scatter count; re-trigger possible
- Important XLSX:
frosted_market_bonanza_v2_96.5.xlsx - Important Sheets:
Base Game,Free Game feature,Ante,Ante Free Game feature,Buy Bonus feature,BaB Trigger,Result - Parser: feg_parse-maths/src/commands/frosted-market-bonanza.commands.ts
- Parser Entry Point: feg_parse-maths/src/main.ts +
CliModule - Parsed Math Object: feg_game_be/games/frostedmarketbonanza/src/interfaces.ts
- Game Math: feg_game_be/games/frostedmarketbonanza/src/maths/frosted-market-bonanza-R3.json
- Game Logic: feg_game_be/games/frostedmarketbonanza/src/logic.ts
- Request:
GameSpinInput - Response:
SpinResult - Important Functions:
FrostedMarketBonanzaPlugin.spin(),FrostedMarketBonanzaEngine.executeSpin(),FrostedMarketBonanzaLogic.spin(),applySymbolConversion(),FrostedMarketBonanzaLogic.runFeature()
23. Bottom line
The actual flow in this repository is not a magical Excel-to-engine conversion. It is a deliberate, range-based transformation pipeline:
- the source math workbook is specific and mode-based,
- the parser reads exact named ranges,
- the parser maps symbols into canonical IDs,
- the runtime loads the generated JSON,
- the game logic applies cluster rules, cascades, scatters, and feature logic,
- the player receives a
SpinResultobject with the final outcome.
The most important rule for a new developer is: trust the parsed JSON and the runtime code, not the workbook labels alone. The codebase is the source of truth about what the game actually does.