Kanıtlanabilir Şekilde Adil

Sonuçlarınızı doğrulamak için oyun detaylarını girin

Kod

Sonuçlarını üreten kaynak kodunu incelemek için yukarıdaki bir oyunu seçin.

import { createHmac } from 'crypto';
import Decimal from 'decimal.js';

const UINT32_MAX = 0x100000000; // 2^32

/**
 * Generates an infinite deterministic stream of cryptographically secure
 * 32-bit unsigned integers derived from HMAC-SHA256.
 *
 * @param key - Secret cryptographic key (server seed). Must remain private until revealed.
 * @param message - Public input string (e.g., client seed, nonce, or domain label).
 *
 * @remarks
 * - Uses HMAC-SHA256 as a pseudorandom function (PRF) to produce a reproducible
 *   and verifiable sequence of numbers.
 * - `step` ensures each 32-bit block is derived from a unique input.
 * - Output is split into 32-bit chunks, suitable for unbiased integer or bit-level
 *   extraction.
 * - Bits are consumed starting from the Most Significant Bit (MSB) to preserve
 *   full numeric precision and maximum entropy.
 * - Fully deterministic: anyone with the same key and message can verify the results.
 */
function* getUint32Stream(key: string, message: string): Iterator<number> {
  let step = 0;

  while (true) {
    const hmac = createHmac('sha256', key);
    hmac.update(`${message}:${step}`);
    const hash = hmac.digest(); // 32 bytes (256 bits)

    for (let i = 0; i < hash.length; i += 4) {
      yield (
        (hash[i] << 24) |
        (hash[i + 1] << 16) |
        (hash[i + 2] << 8) |
        hash[i + 3]
      ) >>> 0; // force unsigned 32-bit
    }

    step++;
  }
}

/**
 * Converts a 32-bit random number into an unbiased integer within [0, range).
 *
 * @param uint32StreamFactory - Function that returns a stream of 32-bit integers.
 * @param range - The desired upper bound (exclusive) for the output.
 *
 * @remarks
 * - Uses rejection sampling to avoid modulo bias.
 * - Every integer within the range is equally likely.
 * - Critical for fair gameplay and provably fair verification.
 */
function getUnbiasedInt(uint32StreamFactory: () => Iterator<number>, range: number) {
  if (!Number.isInteger(range) || range <= 0) {
    throw new Error('Invalid range');
  }

  const maxAcceptable = UINT32_MAX - (UINT32_MAX % range);
  const stream = uint32StreamFactory();
  let next = stream.next();

  while (!next.done) {
    const value = next.value;
    if (value < maxAcceptable) {
      return value % range;
    }
    next = stream.next();
  }

  throw new Error('Failed to generate unbiased value: entropy stream exhausted');
}

/**
 * Generates the sequence of left/right moves for a Plinko ball.
 *
 * @param getUint32Stream - Function returning a cryptographically secure 32-bit stream.
 * @param rows - Number of rows in the Plinko board.
 * @returns Array of 0|1 where 0 = left, 1 = right.
 *
 * @remarks
 * - Each move is determined by a single bit extracted from the 32-bit stream.
 * - Bits are consumed starting from the MSB to maintain maximum precision.
 * - Fully deterministic for given seeds and nonce, enabling provable fairness.
 * - The distribution of left/right moves is perfectly uniform.
 */
function generatePlinkoPath(getUint32Stream: () => Iterator<number>, rows: number) {
  if (!Number.isInteger(rows) || rows <= 0) throw new Error('Invalid rows');
  const path: (0 | 1)[] = [];
  const stream = getUint32Stream();

  let buffer = 0;
  let bitsAvailable = 0;

  while (path.length < rows) {
    if (bitsAvailable === 0) {
      const next = stream.next();
      if (next.done) throw new Error('Entropy stream exhausted');
      buffer = next.value;
      bitsAvailable = 32;
    }
    const bit = (buffer >>> (bitsAvailable - 1)) & 1;
    path.push(bit as 0 | 1);
    bitsAvailable--;
  }

  return path;
}

const MAX_WEIGHT_DECIMALS = 6;
const WEIGHT_SCALE = 10 ** MAX_WEIGHT_DECIMALS;

function countWeightDecimals(n: number) {
  const s = n.toString();

  if (!s.includes('.')) return 0;

  return s.length - s.indexOf('.') - 1;
}

/**
 * Picks a value from a list of weighted segments fairly.
 *
 * @param getUint32Stream - Function returning a cryptographically secure 32-bit stream.
 * @param segments - Array of { value, weight } objects where higher weight = higher chance.
 *
 * @remarks
 * - Weights are scaled to prevent floating-point errors.
 * - Uses `getUnbiasedInt` to avoid bias in weighted selection.
 * - Ensures that rare outcomes are as rare as intended.
 * - Suitable for Rain Mode or any multiplier distribution in the game.
 */
function pickWeighted<T>(getUint32Stream: () => Iterator<number>, segments: { value: T; weight: number }[]) {
  if (!segments.length) throw new Error('No segments provided');

  const scaledWeights: number[] = [];
  let totalWeight = 0;

  for (const { value, weight } of segments) {
    if (!Number.isFinite(weight) || weight <= 0) throw new Error(`Invalid weight for value ${value}`);
    const decimals = countWeightDecimals(weight);
    if (decimals > MAX_WEIGHT_DECIMALS) throw new Error(`Weight for value ${value} exceeds max ${MAX_WEIGHT_DECIMALS} decimals`);

    const scaled = new Decimal(weight).mul(WEIGHT_SCALE).toNumber();
    if (!Number.isInteger(scaled)) throw new Error(`Invalid precision for value ${value}`);

    scaledWeights.push(scaled);
    totalWeight += scaled;
    if (!Number.isSafeInteger(totalWeight)) throw new Error('Total weight exceeds safe integer');
  }

  const ticket = getUnbiasedInt(getUint32Stream, totalWeight);

  let cumulative = 0;
  for (let i = 0; i < scaledWeights.length; i++) {
    cumulative += scaledWeights[i];
    if (ticket < cumulative) return segments[i].value;
  }

  throw new Error('Weighted selection logic failure');
}

interface BallDetail {
  ballPath: (0 | 1)[];
  bucketIndex: number;
  bucketMultiplier: number;
  rouletteMultiplier: number | null;
  accumulatedMultiplier: number;
}

// Adjust desired PLINKO_CONFIGS
const PLINKO_CONFIGS = {
  low: [{
    rowCount: 8,
    multipliers: [1, 1, 1, 1, 1, 1, 1, 1, 1],
  }],
  medium: [{
    rowCount: 8,
    multipliers: [1, 1, 1, 1, 1, 1, 1, 1, 1],
  }],
  high: [{
    rowCount: 8,
    multipliers: [1, 1, 1, 1, 1, 1, 1, 1, 1],
  }],
  rain: [{
    rowCount: 8,
    multipliers: [1, -1, 1, 1, 1, 1, 1, -1, 1],
  }]
} satisfies Record<'low' | 'medium' | 'high' | 'rain', { rowCount: number; multipliers: number[] }[]>;

// Adjust desired PLINKO_RAIN_MULTIPLIERS
const PLINKO_RAIN_MULTIPLIERS = [
  { multiplier: 1, weight: 50 },
  { multiplier: 1, weight: 50 }
] satisfies { multiplier: number; weight: number }[];

/**
 * Calculates the outcome of a Plinko drop, including multipliers and rain mode logic.
 *
 * @remarks
 * - Determines the Plinko ball path using `getBallPath` with seeds and nonce.
 * - Calculates which bucket the ball lands in (bucketIndex) by summing left/right moves.
 * - Fetches the multiplier for that bucket from `PLINKO_CONFIGS`.
 * - Accumulates multipliers if multiple balls have been played.
 * - Handles Rain Mode:
 *   - Buckets with multiplier `-1` trigger a weighted random multiplier from `PLINKO_RAIN_MULTIPLIERS`.
 *   - Recursively calls itself up to a maximum of 50 rounds to prevent stack overflow.
 * - All randomness comes from cryptographically secure `getUint32Stream`.
 * - Fully deterministic for given seeds and nonce, allowing provably fair verification.
 * - Throws descriptive errors if:
 *   - rowCount has no matching configuration
 *   - recursion depth exceeds limit
 *   - multiplier is undefined
 */
function determineResultDetails(options: {
  difficulty: 'low' | 'medium' | 'high' | 'rain';
  rowCount: number;
  roundIndex: number;
  serverSeed: string;
  clientSeed: string;
  nonce: number;
  ballDetails: BallDetail[];
}) {
  if (options.roundIndex > 50) {
    throw new Error('Rain recursion overflow');
  }

  const lastBallDetail = options.ballDetails[options.ballDetails.length - 1];

  const isRainMode = options.difficulty === 'rain';


  let totalNonce: string = options.nonce.toString();

  if (isRainMode) {
    totalNonce += `:${options.roundIndex}`;
  }

  const normalModeUint32Generator = getUint32Stream(options.serverSeed, `${options.clientSeed}:${totalNonce}:plinko`);

  const ballPath = generatePlinkoPath(
    () => normalModeUint32Generator,
    options.rowCount
  );

  const segment = PLINKO_CONFIGS[options.difficulty].find(
    (segment) => segment.rowCount === options.rowCount
  );

  if (!segment) {
    throw new Error(
      `Could not find multipliers for rowCount=${options.rowCount} and difficulty=${options.difficulty}`
    );
  }

  const bucketIndex = ballPath.reduce<number>((acc, init) => acc + init, 0);
  const bucketMultiplier = segment.multipliers[bucketIndex];

  if (bucketMultiplier == null) {
    throw new Error(
      `Could not find multiplier for bucketIndex=${bucketIndex}`
    );
  }

  if (bucketMultiplier !== -1) {
    const accumulatedMultiplier = lastBallDetail
      ? Decimal(lastBallDetail.accumulatedMultiplier)
          .mul(bucketMultiplier)
          .toDecimalPlaces(2, Decimal.ROUND_FLOOR)
          .toNumber()
      : bucketMultiplier;

    options.ballDetails.push({
      ballPath: ballPath,
      bucketIndex,
      bucketMultiplier,
      accumulatedMultiplier,
      rouletteMultiplier: null,
    });

    return options.ballDetails;
  }

  const rainModeRouletteUint32Generator = 
    getUint32Stream(options.serverSeed, `${options.clientSeed}:${options.nonce}:${options.roundIndex}:rain:plinko`);

  const rainMultiplier = pickWeighted(
    () => rainModeRouletteUint32Generator,
    PLINKO_RAIN_MULTIPLIERS.map((m) => ({ value: m.multiplier, weight: m.weight }))
  );

  const accumulatedMultiplier = lastBallDetail
    ? Decimal(lastBallDetail.accumulatedMultiplier)
      .mul(rainMultiplier)
      .toDecimalPlaces(2, Decimal.ROUND_FLOOR)
      .toNumber()
    : rainMultiplier;

  options.ballDetails.push({
    ballPath: ballPath,
    bucketIndex,
    bucketMultiplier,
    accumulatedMultiplier,
    rouletteMultiplier: rainMultiplier,
  });

  return determineResultDetails({
    ...options,
    roundIndex: options.roundIndex + 1,
  });
}

// console.log(
//   determineResultDetails({
//     difficulty: 'low',
//     rowCount: 8,
//     clientSeed: 'player_input_client_seed',
//     serverSeed: 'player_input_server_seed',
//     nonce: 0,
//     roundIndex: 0,
//     ballDetails: []
//   })
// )