rand

某ゲームの乱数バグの件で過去にそういうのがなかったか調べてみたら、ちょっと面白いものが見つかった。

SFC/ワンダースワン版のロマンシングサガは、とてもシンプルな式の乱数を使っているらしい。

Pythonで書き直すとこんな感じか。

def rand_romancingsaga():
    n = 0
    x = 0
    while 1:
        x = (3 * x + n % 15) % 256
        n += 1
        yield x

g = rand_romancingsaga()
while 1:
    print g.next(),

# 0 1 5 18 58 179 31 100 52 165 249 246 238 215 147 185 44 134 149 195 ...

1920の周期内に0〜255が7〜8回登場する。この予測を応用すれば「主人公ひとりだけでノーダメージクリア」なんてのも可能らしいが、一見したところは綺麗な乱数だし、普通にゲームをやっている分にはほとんど影響はないように思われる。

でもこの乱数、よく見ると15回の間「偶偶奇奇」を繰り返して、その後15回ごとに偶奇が反転している。ロマンシングサガというゲームの上ではこの性質はたまたま影響を及ぼさなかっただけで、もしこの乱数をそのまま「すごろく」なんかに使い回していたら、件のゲーム同様回収騒ぎになっていたかもしれない。

まあ実のところ、(a * x + c) mod mというのは「線形合同法」と呼ばれる乱数で、大昔のゲームばかりでなく、C標準のrand()でもほとんど同じようなことをやっている。おそらくほとんどのゲームはこの系の乱数を使っているのではないかと思う。

ただ、式を見れば何となく分かりそうだが、そのまま使うと奇数偶数が周期的に出てしまうから、ビットシフトして上位16ビットを返すとか、定期的にまたは時刻などを見てaやcを取り替えるとか、そこそこ質のいい乱数に見せかけるテクニックが必要になる。

rand_romancingsaga()はcを1回ごとに0から14まで変えているが下位ビットの周期性は残ってしまっている。たぶんこの関数のメリットは、MUL命令のない8ビットCPUでも8ビットのそこそこ使える乱数が得られるというものだろう。一方、rand()は上位16ビットを返すことでサイコロなどに使ったときの偏りが出ない代わりに、32ビットまで計算しておきながら2^15種類の乱数しか作れない。

件のゲームは、rand()とか使わず、過去に作った線形合同法を使うライブラリを使い回している、ということなんだろうか。たまに奇数偶数が入れ替わることがあるそうなので、aやcを取り替える処理はしているのかも。ただ、その入れ替わりのタイミングやaやcの値が適切ではなくて周期が目立ち、あまり注意せずにテストをしたため見逃してしまい、さらにはそれ以外のバグがあまりにも多かったため話題になってしまった、ということなのかもしれない。