スポーツ

ゲーム

一般

カスタムゲーム - プロバブリーフェア

カスタムゲームでの使用方法

YEET Originalのすべてのベットは、ラウンドシードから始まります。ラウンドシードは、プレイヤーのクライアントシードとベットのナンス(nonce)をHMAC-SHA256でハッシュ化したもので、YEETが選択したサーバーシードが鍵として使用されます。ゲームはそのベットに必要なすべての乱数値をこのラウンドシードから生成します。サーバーシードはプレイ前にハッシュ化された状態で表示され、シードペアを更新すると公開されるため、過去のすべてのベットのラウンドシードを再計算し、結果がベット後に改ざんされていないことを確認できます。

他の検証ツールでは一致しない場合があります

他カジノ向けに作られた検証サイトでは乱数の生成方法が異なるため、そこでYEETのベットを確認しても異なる結果が出る可能性があります。YEETのベットは、ゲームの公平性ウィンドウの「検証」タブ、またはこのページのコードを使用して確認してください。

ランダム数値生成

各ベットは2つのステップで生成されます。まず、シードペアとベットのナンス(nonce)から1つのラウンドシードが生成されます。次に、そのラウンドシードからランダムなバイト列が生成され、ゲームはそのストリームから偏りのない整数を読み取ります。同じ入力からは常に同じ数値が導き出されるため、結果の検証が可能となります。

1. 入力値

  • サーバーによって生成されたランダムな64文字の16進数文字列
  • いつでも変更できる文字列
  • ゲームをプレイするごとに増加するカウンター
  • カーソル — ラウンドシードメッセージの最後の部分です。常に0となります。

2. ラウンドシード

ラウンドシードは、サーバーシード(32バイト)を鍵とし、clientSeed:nonce:0というテキストをHMAC-SHA256でハッシュ化したものです。結果は32バイトとなり、64文字の16進数で表記されます。1つのベットにつき、ラウンドシードは必ず1つ存在します。

async function deriveGameSeed(serverSeed, clientSeed, nonce) {
  const seedBytes = hexToBytes(serverSeed);
  if (seedBytes.length !== 32) {
    throw new Error(`Server seed must be 32 bytes (64 hex characters), got ${seedBytes.length}.`);
  }

  const key = await crypto.subtle.importKey(
    'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  const message = new TextEncoder().encode(`${clientSeed}:${nonce}:0`);
  return bytesToHex(new Uint8Array(await crypto.subtle.sign('HMAC', key, message)));
}

3. ラウンドシードからのエントロピー

ラウンドシードは2つ目のHMAC-SHA256の鍵となります。各呼び出しで8バイトのカウンター(リトルエンディアン、0から開始)に署名し、32バイトのブロックを生成します。カウンターは1ブロックごとに1ずつ増加します。読み取りは現在のブロックから順番にバイトを取得します。ブロック内に必要なバイト数が足りない場合、残りのブロックはスキップされ、次のブロックが生成されます。

class GameEntropy {
  constructor(seedBytes) {
    if (seedBytes.length !== 32) throw new Error('Seed must be exactly 32 bytes (64 hex characters).');
    this.buffer = new Uint8Array(32);
    this.offset = 32;
    this.cursor = 0;
    this.key = crypto.subtle.importKey(
      'raw', toArrayBuffer(seedBytes), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
    );
  }

  async next(count) {
    if (count < 1 || count > 32) throw new RangeError('count must be between 1 and 32');

    if (this.offset + count > 32) {
      const message = new ArrayBuffer(8);
      const view = new DataView(message);
      view.setUint32(0, this.cursor >>> 0, true);
      view.setUint32(4, Math.floor(this.cursor / 0x100000000) >>> 0, true);

      const hash = await crypto.subtle.sign('HMAC', await this.key, message);
      this.buffer.set(new Uint8Array(hash));
      this.cursor++;
      this.offset = 0;
    }

    const slice = this.buffer.slice(this.offset, this.offset + count);
    this.offset += count;
    return slice;
  }
}

4. 公平な数値

結果は整数として抽出されます。readUint32 は4バイトを符号なし32ビット整数(リトルエンディアン)として読み取ります。unbiasedUint(entropy, n) は0からn−1までの数値を返し、それぞれ確率は均等です。nが2の累乗である場合、剰余が直接採用されます。それ以外の場合は、32ビットに収まるnの最大の倍数未満の値のみが受け入れられ、それ以上の値が出た場合は再読み込みされるため、結果に偏りは生じません。

async function readUint32(entropy) {
  const bytes = await entropy.next(4);
  return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true);
}

async function unbiasedUint(entropy, range) {
  if (range <= 0) throw new Error('Range must be positive');
  if (range === 1) return 0;

  if ((range & (range - 1)) === 0) {
    return (await readUint32(entropy)) % range;
  }

  const maxValue = 0xffffffff;
  const threshold = maxValue - (maxValue % range) - 1;

  for (;;) {
    const raw = await readUint32(entropy);
    if (raw <= threshold) return raw % range;
  }
}

5. ヘルパー

function hexToBytes(hex) {
  if (typeof hex !== 'string') throw new TypeError('Hex value must be a string.');
  const clean = hex.trim().replace(/^0x/i, '');
  if (clean.length % 2 !== 0) throw new Error('Hex value must have an even length.');
  if (!/^[0-9a-fA-F]*$/.test(clean)) throw new Error('Hex value contains non-hex characters.');

  const bytes = new Uint8Array(clean.length / 2);
  for (let i = 0; i < bytes.length; i++) {
    bytes[i] = parseInt(clean.substring(i * 2, i * 2 + 2), 16);
  }
  return bytes;
}

function bytesToHex(bytes) {
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}

function toArrayBuffer(bytes) {
  return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
}

6. ベットの検証

  • 履歴からベットを開き、クライアントシード、ナンス、およびハッシュ化されたサーバーシードをコピーしてください。
  • シードペアを更新してサーバーシードを公開し、そのSHA-256ハッシュがベット前に確認したものと一致することを確認してください。
  • サーバーシード、クライアントシード、およびナンスを指定して deriveGameSeed を実行し、ラウンドシードを取得してください。
  • ラウンドシードから GameEntropy を作成し、ゲームの変換(「変換」を参照)を適用して、結果をベット内容と比較してください。