Appearance
Plinko Responses
Apply the shared frontend requirements for loading, stakes, operator events, jurisdiction controls, and recovery.
This guide explains how to read the scenario field that placeBet returns for a plinko game. It also explains how to use the loadConfig data to map bucket indexes to multiplier values.
Overview
Plinko is a ball-drop game where a ball falls through rows of pegs into prize buckets. Each combination of row count and risk level (low/medium/high) produces a different set of bucket multipliers. The minRows and maxRows fields in the config give the row counts that the game offers. The shipped builder templates use 8 to 16 rows. The game uses buy features to let the player select a row/risk combination.
The Plinko Scenario
When placeBet returns a plinko result, gameResult.scenario contains:
typescript
interface IPlinkoScenario {
bucket: number; // Bucket index the ball landed in (0 to rows)
rows: number; // Number of rows played (minRows to maxRows)
risk: string; // Risk level: "low" | "medium" | "high"
multiplier: number; // Payout multiplier for this bucket
}typescript
const response = await placeBet({ config, backendURL, token, stake, featureToBuy: 'rows_8_risk_low' });
if (response.success) {
const { scenario, totalWin } = response.result.result;
const plinko = scenario as IPlinkoScenario;
console.log(`Rows: ${plinko.rows}, Risk: ${plinko.risk}`);
console.log(`Ball landed in bucket ${plinko.bucket}`);
console.log(`Multiplier: ${plinko.multiplier}x`);
console.log(`Win: ${totalWin * stakeAmount}`);
}Bucket Multipliers from loadConfig
The loadConfig response includes four plinko fields in the config: minRows, maxRows, risks and multipliers. minRows and maxRows give the lowest and the highest row count. risks lists the risk levels. Together they tell you which row/risk combinations exist. multipliers holds a pre-computed multiplier array for every combination. It is a flat object, not a nested one. Each key has the form rows-{rows}-risk-{risk}. Use this data to render the bucket labels in your UI before the player drops the ball.
typescript
const configResponse = await loadConfig({ backendURL, token });
if (configResponse.success) {
const config = configResponse.result.config;
const minRows = config.minRows as number; // e.g. 8
const maxRows = config.maxRows as number; // e.g. 16
const risks = config.risks as string[]; // ["low", "medium", "high"]
const multipliers = config.multipliers as Record<string, number[]>;
// multipliers["rows-8-risk-low"] → [2.48, 1.48, 1.08, 0.91, 0.86, 0.91, 1.08, 1.48, 2.48]
// multipliers["rows-8-risk-medium"] → [5.08, 1.99, 1.13, 0.83, 0.75, 0.83, 1.13, 1.99, 5.08]
// multipliers["rows-8-risk-high"] → [9.97, 2.58, 1.14, 0.73, 0.63, 0.73, 1.14, 2.58, 9.97]
// multipliers["rows-16-risk-high"] → [281.53, 46.44, 12.53, 4.6, 2.14, 1.21, ...] (17 buckets)
}Each array has rows + 1 entries (one per bucket). Multipliers are symmetric. Edge buckets pay more than center buckets. The values above are the builder defaults for a 98% target RTP. Every game ships its own values, so read them from the config.
Rendering bucket labels
typescript
function renderBuckets(rows: number, risk: string, multipliers: Record<string, number[]>) {
const mults = multipliers[`rows-${rows}-risk-${risk}`];
// mults.length === rows + 1
for (let i = 0; i < mults.length; i++) {
renderBucketLabel(i, `${mults[i]}x`);
}
}Selecting Row/Risk via Buy Features
Every row/risk combination is a buy feature. There is no default basegame. Every placeBet call must include a featureToBuy. If a call does not include one, the engine rejects it with PARAMETERMISSING ("This game requires a buy feature. Provide featureToBuy.").
typescript
import { placeBet } from '@hizi.io/engine-sdk';
// Buy features are named like "rows_12_risk_high"
const featureId = `rows_${selectedRows}_risk_${selectedRisk}`;
const response = await placeBet({
config,
backendURL,
token,
stake,
featureToBuy: featureId,
});Animating the Ball Drop
The scenario gives you the final bucket index and the rows count. To animate the ball path, simulate the descent:
typescript
function animateBallDrop(rows: number, bucket: number) {
// The ball makes `rows` binary decisions (left/right).
// The bucket index equals the number of right turns.
// Generate a random path that ends at the target bucket:
const rights = bucket; // Total right turns needed
const lefts = rows - bucket; // Total left turns needed
// Shuffle to create a random-looking path to the known destination
const moves = [...Array(rights).fill('right'), ...Array(lefts).fill('left')];
shuffleArray(moves);
// Animate each peg bounce
let x = 0;
for (let row = 0; row < rows; row++) {
if (moves[row] === 'right') x++;
animateBounce(row, x);
}
// Ball lands in bucket at index x (=== bucket)
}Single-Result Game
Plinko is a single-result game. Each placeBet call returns a complete result. engineData.inProgress is false after each spin. The game has no multi-step scenarios or continuation calls.
typescript
const response = await placeBet({
config,
backendURL,
token,
stake,
featureToBuy: featureId,
});
if (response.success) {
const { scenario, engineData, totalWin } = response.result.result;
// engineData.inProgress === false - round is complete
// No need to call placeBet again
}Complete Example
typescript
import { login, loadConfig, placeBet } from '@hizi.io/engine-sdk';
interface IPlinkoScenario {
bucket: number;
rows: number;
risk: string;
multiplier: number;
}
// After login and loadConfig...
const config = configResponse.result.config;
const multipliers = config.multipliers as Record<string, number[]>;
// Player selects 12 rows, high risk
const rows = 12;
const risk = 'high';
const featureId = `rows_${rows}_risk_${risk}`;
// Show bucket multipliers before spin
const mults = multipliers[`rows-${rows}-risk-${risk}`];
renderBuckets(mults); // [51.6, 10.26, 3.39, 1.55, 0.91, 0.67, 0.61, 0.67, 0.91, 1.55, 3.39, 10.26, 51.6]
// Place the bet
const response = await placeBet({
config,
backendURL,
token,
stake,
featureToBuy: featureId,
});
if (response.success) {
const plinko = response.result.result.scenario as IPlinkoScenario;
const totalWin = response.result.result.totalWin;
// Animate ball drop through 12 rows into bucket
await animateBallDrop(plinko.rows, plinko.bucket);
// Show result
highlightBucket(plinko.bucket, `${plinko.multiplier}x`);
showWin(totalWin * stake);
}Provably Fair Verification
Plinko is one-shot: a single weighted entry draw lands the ball in its bucket. Use pfVerify to replay that draw from the committed seeds.
Request
typescript
import { pfVerify } from '@hizi.io/engine-sdk';
const reply = await pfVerify({
backendURL,
token,
serverSeed: pf.revealedServerSeed,
clientSeed: pf.clientSeed,
stake: 100,
featureToBuy: 'rows_12_risk_high', // the round's row-count + risk package
});Reading the response
This is one-shot: steps has one entry.
rngData: oneintrow (the entry draw). Compare it to the livepf.rngDatawith an array-equal check.steps[0].scenario.bucket(or whatever field your plinko build exposes): this value equals the live bucket.steps[0].totalWin: this value equals the live multiplier.
typescript
if (!reply.success) return;
const { rngData, steps } = reply.result;
const rngOk = deepEqual(rngData, pf.rngData);
const bucketOk = deepEqual(steps[0].scenario, liveScenario);Next Steps
- Response Handling: the
IGameResultstructure andengineDatafields. - Slot Game Responses: how to read hizi engine slot game responses.
- Buy Features: how buy features work.