JavaScriptの乱数は少し貧弱で、シード値の指定ができないなどの制限があります。普段は問題ないのですが、特定の用途では問題となる場合があります。シード値を指定可能な、再現性のある乱数を自作してみましょう。
JavaScript標準の乱数
もちろんJavaScriptにも乱数を発生させる機能はあります。Math.random()がそれにあたります。
const a = Math.random();
console.log(a);
Math.random()は0.0以上1.0未満の乱数を生成します。しかしこの乱数には問題があります。シード値を指定できず、実行ごとに異なる値が生成されます。
……「乱数」なんだからそれでいいんじゃないの?と思うかもしれません。しかしこれは特定の領域において問題となります。
例えば研究のためにJavaScriptでプログラムを組んだとします。その中で乱数を使って素晴らしい結果を出しました。しかし乱数を再現できないので、二度とその結果を再現することができません!
他にも、例えばゲームを製作しているとき、リプレイ機能を実装しようとすることを考えてみてください。実際にプレイしたときの乱数と、リプレイ時の乱数が異なっていると、正常にリプレイを再生できません!
シード値を指定可能な乱数生成器
そこで必要になってくるのが、乱数を生成するためのシード値を指定することができる乱数生成器です。これを自作してみましょう。
乱数の生成アルゴリズムには、線形合同法やメルセンヌツイスタなど様々なものがありますが、今回はXorShiftを選択しました。XorShiftは、簡単なコードで実現でき、なおかつ高速で、そこそこ質の高い乱数を生成してくれるアルゴリズムです。
class Random {
constructor(seed = 88675123) {
this.x = 123456789;
this.y = 362436069;
this.z = 521288629;
this.w = seed;
}
// XorShift
next() {
let t;
t = this.x ^ (this.x << 11);
this.x = this.y; this.y = this.z; this.z = this.w;
return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
}
}
XorShiftの実装は、たったこれだけです!x, y, z, wには任意の値をセットします。ただし全ての値がゼロの場合は正常に動作しません。そして、その4つの変数の中から適当にひとつ選び出し、シード値をセットします。あとはアルゴリズム通りに計算する(nextメソッドの中身)だけで完成です。
ひとつ注意点として、オリジナルのXorShiftでは符号無し整数を使っていますが、JavaScriptには符号無し整数はありません。したがってC言語などとは異なる結果になることがあります。例えばJavaScriptのXorShiftでは負の数が出てくる可能性があります。
さて、準備ができたら実際に使用してみましょう。以下のように使用します:
const seed = 1; // 任意のシード値
const random = new Random(seed);
// 乱数を10個生成して表示
for(let i = 0; i < 10; i++) {
const value = random.next();
console.log(value);
}
これを実行すると、だいたい以下のような結果になります:
-638953871
504890836
-1873192399
-1873199053
462149015
-983349865
-1615788072
-978086715
-2133735799
1598121209
乱数列を生成できました!そしてシード値が同じなら、何度やっても同じ乱数列が得られます。もちろん異なるシード値を用いれば異なる結果を得ることができます。
また、Math.random()のように実行するたびに異なる値を得たいのならば、Date.now()の数値をシード値として使うといいでしょう。
任意の範囲の乱数を得る
このようにして得られた乱数には、まだ問題があります。出力される値が全くのデタラメで、場合によっては使いづらいことがあります。例えば1から10までの間の乱数が欲しいのに、504890836という値が出てくるかもしれません!
そこで少し手を加えて、任意の範囲の値が得られるようにしましょう。Randomクラスに、任意の範囲の値を出力するnextInt()メソッドを実装します。
class Random {
constructor(seed = 88675123) {
this.x = 123456789;
this.y = 362436069;
this.z = 521288629;
this.w = seed;
}
// XorShift
next() {
let t;
t = this.x ^ (this.x << 11);
this.x = this.y; this.y = this.z; this.z = this.w;
return this.w = (this.w ^ (this.w >>> 19)) ^ (t ^ (t >>> 8));
}
// min以上max以下の乱数を生成する
nextInt(min, max) {
const r = Math.abs(this.next());
return min + (r % (max + 1 - min));
}
}
簡単ですね!しかしなにやら少しややこしい計算が出てきました。これは一体なにをやっているのでしょう?
nextInt()メソッドの中では、まず通常通りに乱数を生成しています。そしてマイナスが出てきたときに消すために、その絶対値をとっています。これをrとします。
次に、最低でもminとなる乱数が欲しいので、minの値をベースに計算していきます。
そして乱数rを(max + 1 – min)で割った余りを求めてminに足しています。「余り」というのは面白いもので、例えばr % 10は必ず0〜9の範囲になります。なぜなら10で割ったとき、余りが10以上になることはないからです。もし10以上の余りがあるのなら、さらに10で割れますからね。
では、さて(max + 1 – min)とはなんでしょう?まず、r % (max + 1)を考えてみましょう。これはrを(max + 1)で割った余りなので、結果は0〜maxになります。そしてそこからminだけ引いているので、r % (max + 1 – min)の結果は0〜max-minとなります。
そしてmin + (r % (max + 1 – min))なので、min + (0〜max-min)となり、これを計算するとmin〜maxとなります。つまり、最小値から最大値の間の値が得られる、ということです。
min + (r % (max – min + 1))の謎は解けました!あとは使ってみるだけですね。
const seed = 1;
const random = new Random(seed);
for(let i = 0; i < 10; i++) {
const value = random.nextInt(2,10);
console.log(value);
}
このとき以下のような出力が得られます:
7
9
9
3
7
3
2
8
6
4
これで任意の範囲の乱数を得ることができました。