HOWTO · JavaScript
JavaScript でシードを使用して乱数を生成する
この記事では、シードを使用して JavaScript で乱数を生成する方法に取り組みます。 これは、シードを受け取り、そのシードに基づいて乱数を返す PRNG のおかげで実現しています。
このページの内容
JavaScriptで再現可能な疑似乱数列を生成するには、シード付き生成器を初期化し、毎回同じシード、生成器の実装、呼び出し順序を使います。Math.random()にはシードを渡せないため、以下の例のようなシード付きPRNGか、seedrandom.jsを使用してください。
シード付き乱数の仕組み
JavaScriptのMath.random()にはシードを渡せません。乱数列を再現するには、初期状態を受け取る疑似乱数生成器(PRNG)を作成します。同じシード、同じ実装、同じ呼び出し順序で生成器を呼び出せば、同じ乱数列が得られます。これらの入力のいずれかを変えると、その後の値も変わることがあります。
シードは生成器を初期化するものであり、それ自体が乱数なのではありません。文字列などの扱いやすい入力をシードに変換する処理と、生成器の状態を進めるアルゴリズムは分けて考えましょう。以下の例では、シミュレーション、デモ、テストで再現可能な結果を得るための、汎用的な小型PRNGを使います。
文字列を生成器のシードに変換する
SFC32とXoshiro128**は、32ビットの状態語を4つ使います。そのため、これらの生成器を初期化する前に、文字列を4つの数値に変換する必要があります。ここで使うコンパクトなハッシュはその方法の1つですが、暗号学的ハッシュではなく、秘密情報の保護には使えません。固定幅のハッシュでは、ほかの文字列ハッシュと同様に異なる文字列が衝突することがあります。
この関数は、各UTF-16コード単位を4つの符号なし32ビット値に混ぜ込みます。得られる値は決定的な例の状態として使えますが、エントロピーを追加したり、生成器を安全にしたりするものではありません。同じ初期状態を再現するには、毎回同じ文字列と関数を使ってください。
このセクションのコードと表示されるラベルや出力は英語のままです。
function hashStringToWords(string) {
let a = 0x9e3779b9;
let b = 0x243f6a88;
let c = 0xb7e15162;
let d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b);
b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f);
d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13;
b ^= c >>> 11;
c ^= d >>> 15;
d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
const words = hashStringToWords("demo seed");
console.log(words.join(", "));
生成器ごとに必要な状態値も異なります。SFC32とXoshiro128**は4つの状態語を受け取り、Mulberry32は32ビット値を1つ受け取ります。以下のJSFの例では、1つの値から4つの初期値を作り、値を返す前に状態をウォームアップします。同じ値を4回渡せばどの生成器も正しく初期化される、と考えてはいけません。
SFC32、Mulberry32、Xoshiro128**、JSFで数値を生成する
これらの関数は、0以上1未満の値を返します。SFC32とXoshiro128**には、hashStringToWordsが返す4つの値を渡します。Mulberry32とJSFには、最初の値を使います。各関数は変更可能な状態を内部に保持するため、列を最初から再生成するには新しい生成器を作成してください。
SFC32は、すべてゼロの状態から始めた場合も含め、呼び出すたびに4つの32ビット値を更新します。Xoshiro128**にはゼロ以外の状態が必要です。ハッシュ関数は、ここで示すdemo seedに対してゼロではない値を4つ返します。
このセクションのコードと表示されるラベルや出力は英語のままです。
function hashStringToWords(string) {
let a = 0x9e3779b9;
let b = 0x243f6a88;
let c = 0xb7e15162;
let d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b);
b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f);
d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13; b ^= c >>> 11; c ^= d >>> 15; d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
function sfc32(a, b, c, d) {
return function () {
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
let t = (a + b) | 0;
a = b ^ (b >>> 9);
b = (c + (c << 3)) | 0;
c = (c << 21) | (c >>> 11);
d = (d + 1) | 0;
t = (t + d) | 0;
c = (c + t) | 0;
return (t >>> 0) / 4294967296;
};
}
const random = sfc32(...hashStringToWords("demo seed"));
console.log(random());
console.log(random());
Mulberry32は、状態語を1つだけ使うコンパクトな生成器です。固定定数を加えて状態を進めてから、ビットを混ぜて各結果を作ります。
function mulberry32(seed) {
let state = seed >>> 0;
return function () {
let t = (state += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function hashStringToWords(string) {
let a = 0x9e3779b9;
for (let i = 0; i < string.length; i++) {
a = Math.imul(a ^ string.charCodeAt(i), 0x85ebca6b);
a ^= a >>> 13;
}
return a >>> 0;
}
const random = mulberry32(hashStringToWords("demo seed"));
for (let i = 0; i < 5; i++) console.log(random());
Xoshiro128**は4つの状態語を使い、star-star出力変換を行います。状態遷移と出力計算は別々なので、呼び出しのたびに更新される4つの状態語すべてを保持してください。
function xoshiro128ss(s) {
s = s.map((word) => word >>> 0);
return function () {
const result = Math.imul(((Math.imul(s[1], 5) << 7) |
(Math.imul(s[1], 5) >>> 25)), 9) >>> 0;
const t = s[1] << 9;
s[2] ^= s[0];
s[3] ^= s[1];
s[1] ^= s[2];
s[0] ^= s[3];
s[2] ^= t;
s[3] = (s[3] << 11) | (s[3] >>> 21);
return result / 4294967296;
};
}
function hashStringToWords(string) {
let a = 0x9e3779b9, b = 0x243f6a88, c = 0xb7e15162, d = 0xdeadbeef;
for (let i = 0; i < string.length; i++) {
const k = string.charCodeAt(i);
a = Math.imul(a ^ k, 0x85ebca6b); b = Math.imul(b ^ k, 0xc2b2ae35);
c = Math.imul(c ^ k, 0x27d4eb2f); d = Math.imul(d ^ k, 0x165667b1);
a ^= b >>> 13; b ^= c >>> 11; c ^= d >>> 15; d ^= a >>> 9;
}
return [a, b, c, d].map((word) => word >>> 0);
}
const random = xoshiro128ss(hashStringToWords("demo seed"));
console.log(random());
JSF(Jenkins Small Fast)も4つの状態語を使います。この実装では、最初のハッシュ値から初期状態を作り、値を返す前に状態遷移を20回行います。その後、32ビットの結果を同じ範囲に収まるようにスケーリングして返します。
function jsf32(seed) {
let a = 0xf1ea5eed;
let b = seed >>> 0;
let c = seed >>> 0;
let d = seed >>> 0;
function nextUint32() {
const e = (a - ((b << 27) | (b >>> 5))) | 0;
a = b ^ ((c << 17) | (c >>> 15));
b = (c + d) | 0;
c = (d + e) | 0;
d = (a + e) | 0;
return d >>> 0;
}
for (let i = 0; i < 20; i++) nextUint32();
return () => nextUint32() / 4294967296;
}
function hashStringToWord(string) {
let a = 0x9e3779b9;
for (let i = 0; i < string.length; i++) {
a = Math.imul(a ^ string.charCodeAt(i), 0x85ebca6b);
a ^= a >>> 13;
}
return a >>> 0;
}
const random = jsf32(hashStringToWord("demo seed"));
for (let i = 0; i < 3; i++) console.log(random());
同じ文字列で初期化しても、状態遷移が異なるため、アルゴリズムごとに乱数列は異なります。結果を再現するには、シードだけでなくアルゴリズムと呼び出し順序も同じにしてください。
seedrandom.jsを使う
seedrandom.jsは、シード付き疑似乱数生成のためのJavaScriptライブラリです。以下のブラウザページ用ソースはCDNJSからバージョン3.0.5を読み込み、文字列Johnsonで生成器を作って、最初の値をログに出力します。スクリプトの読み込みにはネットワークに接続できるブラウザが必要です。このコードは応用するためのソースであり、ここでは実行結果を検証していません。
このセクションのコードと表示されるラベルや出力は英語のままです。
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("Johnson");
console.log(random());
</script>
</body>
生成器のインスタンスにはquick()とint32()メソッドもあります。quick()は小数値を返し、int32()は符号付き32ビット整数を返します。呼び出すたびに生成器の状態が進むため、2回目の呼び出しは乱数列の1回目と同じ位置の値を表しません。
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("DavidBau");
console.log("With quick():", random.quick());
console.log("With int32():", random.int32());
</script>
</body>
シード付き、エントロピー混合、自動の動作を選ぶ
同じ入力から再現できる乱数列が必要なら、明示的にシードを指定します。示されているseedrandom.jsの例では、シードとともに{ entropy: true }オプションも渡しています。この例から分かるのはオプションを渡していることだけです。特定のエントロピー源、保証、再現性については示していないため、そうした性質があるとは考えないでください。
このセクションのコードと表示されるラベルや出力は英語のままです。
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/seedrandom/3.0.5/seedrandom.min.js"></script>
<script>
const random = new Math.seedrandom("Antananarivo", { entropy: true });
for (let i = 0; i < 5; i++) console.log(random());
</script>
</body>
再現可能なデモではなく新しい値が必要な場合は、その要件に適した値の取得元を選び、文書化された動作を確認してください。この記事では、上記の呼び出しで示されていない、省略したシード引数やオプションの動作は想定していません。
シード付き疑似乱数の限界
シード付きPRNGは、シミュレーションや決定的なテスト用データなど、プログラムで再現可能な乱数列が必要な場合に役立ちます。これはアルゴリズムであり、予測できない秘密の値を得るための情報源ではありません。シードが予測できれば、生成される乱数列も予測可能です。独自の文字列ハッシュを使っても、この点は変わりません。
パスワード、認証トークン、暗号鍵など、セキュリティ上重要な値の生成にこれらの例を使わないでください。そのような用途では、実行環境とプラットフォームに適した暗号学的に安全な乱数APIを使ってください。また、シードが同じなら異なるPRNG実装でも一致する、と考えないでください。アルゴリズム、シード変換、初期状態、呼び出し回数と順序を保つ必要があります。