Subterranean Flower

JavaScriptで再現性のある乱数を生成する + 指定した範囲の乱数を生成する

Author
古都こと
ふよんとえんよぷよぐやま!!!

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

これで任意の範囲の乱数を得ることができました。