Skip to content

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: one int row (the entry draw). Compare it to the live pf.rngData with 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 ​