はじめに (対象読者・この記事でわかること)

この記事は、JavaScriptを使ってゲーム開発やシミュレーションを行う方、乱数生成の仕組みに興味があるプログラミング中級者、あるいはJavaScriptのビルトイン乱数関数(Math.random())の限界を感じている方を対象としています。

この記事を読むことで、以下の点がわかるようになります。

  • Math.random()の基本的な動作と、なぜそれが「疑似乱数」と呼ばれるのか。
  • 真の乱数ではなく、なぜ「疑似乱数」が多くの場面で利用されるのか。
  • 代表的な疑似乱数生成アルゴリズムである線形合同法(LCG)、Xorshift、そしてメルセンヌ・ツイスターの基本的な考え方と、それぞれの特性。
  • JavaScriptでこれらのアルゴリズムをシンプルに実装する方法。
  • カスタム疑似乱数生成器を作成するメリットと、具体的な活用例。

ビルトインのMath.random()だけでは実現できない、特定のシード値に基づいて再現可能な乱数を生成したり、より高品質な乱数が必要な場面で役立つ知識を提供します。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • JavaScriptの基本的な構文(変数宣言、関数定義、ループ、条件分岐など)
  • ビット演算の基本的な概念(&AND, |OR, ^XOR, <<左シフト, >>符号付き右シフト, >>>符号なし右シフトなど)

Math.random()の限界と疑似乱数の必要性

JavaScriptにおいて、手軽に乱数を生成する方法として広く使われているのがMath.random()です。これは0(含む)から1(含まない)の範囲で浮動小数点数を返します。非常に便利で日常的な用途には十分ですが、いくつか知っておくべき特性があります。

Math.random()は「真の乱数」ではない

まず、Math.random()が生成するのは「真の乱数」ではなく「疑似乱数」です。真の乱数は予測不可能で再現不可能なものですが、コンピューターのプログラムによって生成される乱数は、ある初期値(シード)から決まった計算手順を経て生成されるため、そのシードが同じであれば常に同じ乱数列を生成します。これが「疑似乱数」の定義です。

Math.random()の場合、ブラウザやNode.jsといったJavaScript実行環境が内部的にシードを管理しており、通常はユーザーがシードを指定することはできません。これにより、以下のような制約が生まれます。

  1. 再現性の欠如: 特定のシード値から始まる一連の乱数を再現することができません。例えば、ゲームで生成されるマップをユーザーが同じシードで再生成したい場合や、シミュレーション結果をデバッグのために再現したい場合には不向きです。
  2. 品質の懸念: Math.random()の実装は環境によって異なり、その乱数の「品質」(統計的な偏りがないか、周期が十分に長いかなど)は保証されません。高度な統計解析や暗号技術に使うには不十分な場合があります。
  3. 周期性: すべての疑似乱数生成器には周期があり、いずれ同じ乱数列が繰り返されます。Math.random()の周期は非常に長いとされていますが、無限ではありません。

なぜ疑似乱数が必要なのか?

真の乱数は、物理現象(例: 大気ノイズ、放射性崩壊)に基づいて生成されるため、専用のハードウェアが必要です。ソフトウェアで手軽に利用できるものではありません。一方で、多くのアプリケーションではそこまで厳密な真の乱数は必要なく、むしろ「再現性」が求められることがあります。

  • ゲーム開発: プロシージャル(手続き型)生成されるマップ、アイテム、敵の配置などを、特定のシード値で再現可能にするため。
  • シミュレーション: 科学計算や統計シミュレーションで、同じ条件で何度でも実験結果を再現するため。
  • テスト: ソフトウェアのテストで、特定の乱数列を固定して、バグの再現性を確保するため。

これらの要件を満たすために、私たちはカスタムの疑似乱数生成アルゴリズムを理解し、必要に応じて利用することが重要になります。

主要な疑似乱数生成アルゴリズムとその実装

ここからは、より高度な制御が可能な疑似乱数生成アルゴリズムをいくつか紹介し、JavaScriptでの実装例を見ていきます。これらのアルゴリズムは、シード値を明示的に指定できるため、再現性のある乱数生成が可能になります。

線形合同法 (Linear Congruential Generator: LCG)

線形合同法(LCG)は、最も古く、そして最もシンプルな疑似乱数生成アルゴリズムの一つです。その計算式は非常に単純で理解しやすいですが、その分いくつかの欠点も持ち合わせています。

原理

LCGの基本的な計算式は以下の通りです。

X_{n+1} = (a * X_n + c) mod m
  • X_n: 現在の乱数(または状態)
  • X_{n+1}: 次の乱数(または状態)
  • a: 乗数 (multiplier)
  • c: 加算数 (increment)
  • m: 法 (modulus)
  • mod: 剰余演算

この式を繰り返し適用することで、数値のシーケンスを生成します。X_0が初期のシード値となります。

特性

  • メリット: 実装が非常に簡単で、計算が高速です。
  • デメリット: 生成される乱数の品質は低く、周期が比較的短い傾向にあります。特に、mが2のべき乗の場合、下位ビットに周期的なパターンが出やすくなります。また、適切なa, c, mの組み合わせを選ぶのが難しい場合があります。

JavaScriptでの実装例

Javascript
class LCG { constructor(seed) { this.m = 2 ** 32; // 法 (modulus) this.a = 1664525; // 乗数 (multiplier) this.c = 1013904223; // 加算数 (increment) this.seed = seed || Date.now(); // 初期シード } // 0から1未満の浮動小数点数を生成 next() { this.seed = (this.a * this.seed + this.c) % this.m; return this.seed / this.m; } // 範囲指定の整数を生成 [min, max] nextInt(min, max) { return Math.floor(this.next() * (max - min + 1)) + min; } } // 使用例 const lcg = new LCG(12345); // シード12345で初期化 console.log("--- LCG ---"); for (let i = 0; i < 5; i++) { console.log(lcg.next()); // 0以上1未満の乱数 } console.log(lcg.nextInt(1, 100)); // 1から100の乱数

Xorshift

Xorshiftは、ビット演算(排他的論理和: XORとビットシフト)のみを使用して乱数を生成するアルゴリズムのファミリーです。LCGよりも高速で、より長い周期と優れた統計的特性を持ちます。

原理

Xorshiftの基本は、内部状態を排他的論理和とビットシフトによって更新することです。複数の状態変数を持つバリエーション(例: Xorshift128+, Xorshift64*)が一般的で、より高品質な乱数を生成できます。

特性

  • メリット: 非常に高速で、LCGよりもはるかに長い周期を持ちます。多くのゲームやシミュレーションで利用されています。実装も比較的シンプルです。
  • デメリット: 特定のバリエーションで統計的テストを通過しない場合があるため、アルゴリズムの選択が重要です。メルセンヌ・ツイスターほどではないものの、初期化に注意が必要です。

JavaScriptでの実装例 (Xorshift32)

ここでは最もシンプルなXorshift32を例に挙げます。

Javascript
class Xorshift32 { constructor(seed) { this.x = seed || Date.now(); // 32ビットのシード if (this.x === 0) this.x = 1; // シードが0だと乱数が停止するため } // 0から1未満の浮動小数点数を生成 next() { // Xorshift32の一般的なシフト定数 (例: 13, 17, 5) // 内部状態xを更新 this.x ^= this.x << 13; this.x ^= this.x >>> 17; this.x ^= this.x << 5; // 32ビット符号なし整数として扱い、0から1の範囲に正規化 return (this.x >>> 0) / 0xFFFFFFFF; // >>> 0 で符号なし整数に変換 } // 範囲指定の整数を生成 [min, max] nextInt(min, max) { return Math.floor(this.next() * (max - min + 1)) + min; } } // 使用例 const xorshift = new Xorshift32(54321); // シード54321で初期化 console.log("\n--- Xorshift32 ---"); for (let i = 0; i < 5; i++) { console.log(xorshift.next()); } console.log(xorshift.nextInt(1, 100));

メルセンヌ・ツイスター (Mersenne Twister: MT19937)

メルセンヌ・ツイスター(MT19937)は、現在最も広く使われている汎用疑似乱数生成器の一つです。非常に長い周期(2^19937 - 1)と優れた統計的品質を誇ります。C++の標準ライブラリにも採用されています。

原理

メルセンヌ・ツイスターは、大きな内部状態(624個の32ビット整数)を持ち、線形帰還シフトレジスタ(LFSR)の原理に基づいて次の状態を計算します。ビット操作を巧妙に組み合わせることで、非常に均一な乱数列を生成します。

特性

  • メリット: 周期が非常に長く、均一性や多次元にわたる分布の均一性が非常に優れています。学術研究やシミュレーションなど、高品質な乱数が必要な場面で信頼されています。
  • デメリット: 内部状態が大きく、初期化が複雑です(JavaScriptでゼロから実装するのは手間がかかります)。LCGやXorshiftに比べると、計算はわずかに遅いです。

JavaScriptでの実装(概念とライブラリの利用)

メルセンヌ・ツイスターのJavaScriptでのゼロからの実装は、非常に複雑でコード量も多くなるため、ここではアルゴリズムの概念説明に留め、実際の利用には既存のライブラリを用いることを推奨します。

しかし、その動作のイメージを掴むために、擬似コードや概念的な説明を以下に示します。

  • 内部状態: 624個の32ビット整数を格納する配列(MT[0]からMT[623])。
  • 初期化: シード値からこの配列を初期化します。非常に多くの計算ステップを要します。
  • 乱数生成:
    1. 配列内のMT値を更新し、新しい内部状態を計算します。これは、特定のインデックスの要素と、それより離れたインデックスの要素をビット演算で組み合わせることで行われます。
    2. 計算された新しい状態値に対して、「ツイスト(捻り)」と呼ばれる一連のビット操作(XOR、シフト、マスクなど)を適用します。これにより、乱数の品質がさらに向上します。
    3. ツイストされた値が乱数として出力されます。

ライブラリの利用例:

Node.jsやブラウザ環境でメルセンヌ・ツイスターを使いたい場合は、mersenne-twisterのような既存のnpmパッケージを利用するのが一般的です。

Javascript
// npm install mersenne-twister を実行してインストール // const MersenneTwister = require('mersenne-twister'); // Node.jsの場合 // import MersenneTwister from 'mersenne-twister'; // ES Modulesの場合 // クラス定義の代わりにライブラリの使用例を示す /* const mt = new MersenneTwister(98765); // シード98765で初期化 console.log("\n--- Mersenne Twister (ライブラリ利用を想定) ---"); for (let i = 0; i < 5; i++) { console.log(mt.random()); // 0以上1未満の乱数 } // ライブラリによっては整数生成メソッドも提供 // console.log(mt.random_int()); // 32ビット整数を生成 */ console.log("\n--- メルセンヌ・ツイスター ---"); console.log("(実装が複雑なため、通常はライブラリを利用します。概念説明を参照ください。)"); console.log("例: npm install mersenne-twister など");

カスタム疑似乱数生成器の活用

これらのカスタムアルゴリズムを導入する最大のメリットは、シード値による再現性と、より目的に合った高品質な乱数生成が可能になる点です。

  1. ゲームの再現性: 同じシード値を使えば、プレイヤーが何度でも同じワールドマップや敵の配置を体験できるゲームを作成できます。
  2. シミュレーションの信頼性: 科学的なシミュレーションで、パラメータを変えても乱数列を固定することで、実験結果の比較を正確に行えます。
  3. 効率的なデバッグ: 意図しないバグが乱数によって発生した場合でも、シード値を固定すれば常にそのバグを再現し、原因を特定しやすくなります。
  4. 多様な乱数: 例えば、特定の範囲の整数、正規分布に従う乱数など、用途に合わせた複雑な乱数生成ロジックを、コアの疑似乱数生成器の上に構築できます。

ハマった点やエラー解決

疑似乱数生成器を実装・利用する際に、いくつか陥りやすい落とし穴があります。

  1. シード値の選択ミス:

    • 常に同じシード(例: 01)を使うと、開発時に毎回同じ乱数列になり、テストはしやすいものの、本番環境で多様性が失われます。
    • 逆に、Date.now()のような時間ベースのシードを毎回生成器の初期化に使うと、デバッグ時に乱数を再現できなくなります。
    • 解決策: 本番環境ではDate.now()やUUIDのようなユニークな値をシードとして使い、デバッグや再現性が必要な場合はそのシードを保存して再利用できるようにする。ユーザーがシードを指定できるUIを提供することも検討する。
  2. ビット演算の理解不足:

    • JavaScriptの数値は浮動小数点数(倍精度浮動小数点数)であり、ビット演算を行うと内部的に32ビット整数に変換されて処理されます。特に符号付き(>>)と符号なし(>>>)の右シフトの違い、そして32ビットを超えた値の扱いに注意が必要です。
    • Xorshiftなどで内部状態が負の値になった場合、意図しない結果になることがあります。
    • 解決策: ビット演算の結果を常に32ビット符号なし整数として扱いたい場合は、最後に>>> 0を適用して変換する習慣をつける(例: (this.x >>> 0))。これにより、値が常に0以上の32ビット範囲に収まることを保証できます。
  3. アルゴリズム選択の誤り:

    • 高品質な乱数(統計的に偏りがなく、周期が長い)が必要な場面でLCGのような単純なアルゴリズムを使ってしまい、シミュレーション結果に偏りが出たり、ゲームパターンが予測可能になったりする。
    • 解決策: 必要な乱数の品質とパフォーマンス要件を事前に評価する。高速性が最優先で周期がそこそこで良いならXorshift系、最高の品質と長周期が欲しいならメルセンヌ・ツイスター系、ごく簡単な用途ならLCGといったように、適切なアルゴリズムを選ぶ。

まとめ

本記事では、JavaScriptにおける疑似乱数生成の重要性、そしてビルトインのMath.random()だけでは満たせない高度な要件(再現性、高品質)に対応するためのカスタムアルゴリズムについて解説しました。

  • 要点1: Math.random()は便利ですが、シードの固定や、特定の統計的特性を持つ高品質な乱数が必要な場合は、カスタムの疑似乱数生成器を検討する必要があります。
  • 要点2: 線形合同法(LCG)はシンプルで高速ですが周期が短く品質が低い傾向にあります。Xorshiftはビット演算を利用し、LCGよりも高速で周期が長い優れた選択肢です。メルセンヌ・ツイスターは非常に長い周期と高い均一性を持ち、高品質な乱数が必要な場合に適していますが、実装は複雑です。
  • 要点3: カスタム疑似乱数生成器を導入することで、特定のシード値に基づいてゲームの再現性を確保したり、シミュレーションの品質を向上させたりと、より高度な乱数制御が可能になります。

この記事を通して、読者の皆様は疑似乱数の基本的な概念と、自身のプロジェクトに最適な乱数生成器を選択・実装するための基礎知識を得られたでしょう。

今後は、これらの疑似乱数生成器を実際にゲームのプロシージャル生成や、より複雑な物理シミュレーションに応用する方法、またはWebAssemblyを活用した乱数生成の高速化、さらには暗号論的乱数生成器(CSPRNG)についても記事にする予定です。

参考資料