再現可能な乱数発生器

真の低脳は意外なところに潜む - うさだBlog / ls@usada's Workshop
はてな
こういう記事を受けて。
正直、NScripterで乱数を使う場面と言うのはあまりない。と言うか、普通にノベルゲームをつくっている限り、乱数が必要になることはない。ストーリーはプレイヤーの意思によってのみ左右されこの点において神(サイコロ)の介入する余地はない。はずだ。
それはそれとして、最近ではNScripterをノベルゲーム以外の用途に使うことも普通になってきた(つか俺だ)。よって、そこにおいては乱数の必要性も生じてくる、と思う。
それを踏まえて、線形合同法(参考:線形合同法 - Wikipedia)による乱数発生器をNScripterネイティブで実装してみた。
線形合同法による乱数の質はよくない、のはわかっているが、最下位ビットに注意するだけで、ゲームに使うには充分な精度になるんじゃないかなあと考えている。
しかも、最下位ビットが問題になるのは、出てくる数値をそのまま使う時だけだから、NScripterのrnd関数の仕様に合わせるならば問題は発生しないように思う。

define節

これを、define節、*define以下、game以上に記述する。

; 以下、線形合同法におけるA,B,Mの値。意味がわかるなら変更してもよい。
; 初期設定では、65536回やればまた同じ配列が発生するようになっている。
numalias lcgs_a,257
numalias lcgs_b,19683
numalias lcgs_m,65536
; 使用する変数の番号
numalias lcgs_first_seed,100 ; 適宜変更すること
numalias lcgs_seed,101 ; 適宜変更すること
numalias lcgs_onoff,102 ; 適宜変更すること
numalias lcgs_new,103 ; 適宜変更すること
numalias rnd_min,104 ; 適宜変更すること
numalias rnd_max,105 ; 適宜変更すること
numalias rnd_result,106 ; 適宜変更すること
numalias rnd_width,107 ; 適宜変更すること
numalias rnd_div,108 ; 適宜変更すること
; 上書きないし作成する命令群
defsub rnd2
defsub lcgs_on ; 
defsub lcgs_off ; 上書きした乱数処理を使わない
defsub lcgs_flag ; 現在の状態を取得する。
defsub set_rnd_seed ; 乱数の種を仕込む命令。
defsub get_rnd_seed ; 現在の乱数の種を取得する。
defsub get_first_seed ; set_rnd_seedで設定した値を取得する。

上書きルーチン

これをスクリプトのどこかに書く。define節のgame以下とか、*start節の最後とか。

*rnd
getparam i%rnd_result,%rnd_max
if %lcgs_onoff=0 _rnd %%rnd_result,%rnd_max:return
if %rnd_max=0 mov %%rnd_result,0:return
if %rnd_max<0 mul %rnd_max,-1
mov %rnd_min,0:dec %rnd_max
goto *rnd_main

*rnd2
getparam i%rnd_result,%rnd_min,%rnd_max
if %lcgs_onoff=0 _rnd2 %%rnd_result,%rnd_min,%rnd_max:return
if %rnd_max<%rnd_min mov %%rnd_result,%rnd_max:mov %rnd_max,%rnd_min:mov %rnd_min,%%rnd_result
goto *rnd_main

*rnd_main
mov %rnd_width,%rnd_max-%rnd_min+1 ; 要求される数値の幅の計算
if %rnd_width=1 mov %%rnd_result,%rnd_max:return ; 幅なし処理
mov %rnd_div,lcgs_m/2/%rnd_width
mul %rnd_div,%rnd_width
~
gosub *lcgs
notif %rnd_div>%lcgs_new/2 jumpb
mov %%rnd_result,(%lcgs_new/2 mod %rnd_width)+%rnd_min
return

*lcgs
mov %lcgs_new,(lcgs_a*%lcgs_seed+lcgs_b) mod lcgs_m
mov %lcgs_seed,%lcgs_new
return

*set_rnd_seed
getparam %lcgs_seed
~
if %lcgs_seed<0 add %lcgs_seed,lcgs_m:jumpb
notif %lcgs_seed<lcgs_m sub %lcgs_seed,lcgs_m:jumpb
mov %lcgs_first_seed,%lcgs_seed
return

*get_rnd_seed
getparam i%rnd_result
mov %%rnd_result,%lcgs_seed
return

*get_first_seed
getparam i%rnd_result
mov %%rnd_result,%lcgs_first_seed
return

*lcgs_on
mov %lcgs_onoff,1
return

*lcgs_off
mov %lcgs_onoff,0
return

*lcgs_flag
getparam i%rnd_result
mov %%rnd_result,%lcgs_onoff
return

使い方

これを導入すると、rnd命令とrnd2命令を上書きして、数値の再現が可能な乱数を発生させることができる。
ただし、そのためにはいくつか準備命令が必要。

  1. set_rnd_seed命令で乱数の種を仕込む。(ここで与える数値は0以上lcgs_mの値未満でなければならない。)
  2. その後、lcgs_on命令を実行する。(引数はなし)これによって、今後rnd2を実行した場合は、このライブラリで追加した乱数発生器を使うので、再現可能性が保証される。
  3. lcgs_off命令でNScripter本来のrnd命令とrnd2命令に戻る。
  4. 今現在、どちらのモードになっているかは、lcgs_flag %0で取得できる。0が本来のモード、1が新規追加したモードである。
  5. 最初に与えた乱数の種が何であるかは、get_first_seedで取得できる。
  6. 現在の種、つまり直前に発生された乱数は、get_rnd_seedで取得できる。ただし、この数値は実際に発生させた値ではなく、変換前の乱数であり(初期設定のままならば)0から65535までのどれかの値を取る。
  7. アルゴリズムの細かい点を変更したければ、線形合同法 - Wikipedia読んでいじれ!
  8. なお、発生させられる乱数の最大値(rnd2にあっては、乱数の幅)は、最大でlcgs_mの2分の1になる。なので、現状では0〜32767までの乱数を発生させられることになる。(これは、線形合同法の弱点である最下位ビットを切り捨てるため)これ以上の幅が欲しければ、自分でlcgs_a, lcgs_b, lcgs_mの値を調整すること。
  9. lcgs_onとlcgs_offを入れ替えることで、再現性を必要とする乱数と必要としない乱数を切り替えることができる。lcgs_offをすると、lcgs_onした時点で再開される。

実行サンプル

*define
; 以下、線形合同法におけるA,B,Mの値。意味がわかるなら変更してもよい。
; 初期設定では、65536回やればまた同じ配列が発生するようになっている。
numalias lcgs_a,257
numalias lcgs_b,19683
numalias lcgs_m,65536
; 使用する変数の番号
numalias lcgs_first_seed,100 ; 適宜変更すること
numalias lcgs_seed,101 ; 適宜変更すること
numalias lcgs_onoff,102 ; 適宜変更すること
numalias lcgs_new,103 ; 適宜変更すること
numalias rnd_min,104 ; 適宜変更すること
numalias rnd_max,105 ; 適宜変更すること
numalias rnd_result,106 ; 適宜変更すること
numalias rnd_width,107 ; 適宜変更すること
numalias rnd_div,108 ; 適宜変更すること
; 上書きないし作成する命令群
defsub rnd2
defsub lcgs_on ; 
defsub lcgs_off ; 上書きした乱数処理を使わない
defsub lcgs_flag ; 現在の状態を取得する。
defsub set_rnd_seed ; 乱数の種を仕込む命令。
defsub get_rnd_seed ; 現在の乱数の種を取得する。
defsub get_first_seed ; set_rnd_seedで設定した値を取得する。

game

*rnd
getparam i%rnd_result,%rnd_max
if %lcgs_onoff=0 _rnd %%rnd_result,%rnd_max:return
if %rnd_max=0 mov %%rnd_result,0:return
if %rnd_max<0 mul %rnd_max,-1
mov %rnd_min,0:dec %rnd_max
goto *rnd_main

*rnd2
getparam i%rnd_result,%rnd_min,%rnd_max
if %lcgs_onoff=0 _rnd2 %%rnd_result,%rnd_min,%rnd_max:return
if %rnd_max<%rnd_min mov %%rnd_result,%rnd_max:mov %rnd_max,%rnd_min:mov %rnd_min,%%rnd_result
goto *rnd_main

*rnd_main
mov %rnd_width,%rnd_max-%rnd_min+1 ; 要求される数値の幅の計算
if %rnd_width=1 mov %%rnd_result,%rnd_max:return ; 幅なし処理
mov %rnd_div,lcgs_m/2/%rnd_width
mul %rnd_div,%rnd_width
~
gosub *lcgs
notif %rnd_div>%lcgs_new/2 jumpb
mov %%rnd_result,(%lcgs_new/2 mod %rnd_width)+%rnd_min
return

*lcgs
mov %lcgs_new,(lcgs_a*%lcgs_seed+lcgs_b) mod lcgs_m
mov %lcgs_seed,%lcgs_new
return

*set_rnd_seed
getparam %lcgs_seed
~
if %lcgs_seed<0 add %lcgs_seed,lcgs_m:jumpb
notif %lcgs_seed<lcgs_m sub %lcgs_seed,lcgs_m:jumpb
mov %lcgs_first_seed,%lcgs_seed
return

*get_rnd_seed
getparam i%rnd_result
mov %%rnd_result,%lcgs_seed
return

*get_first_seed
getparam i%rnd_result
mov %%rnd_result,%lcgs_first_seed
return

*lcgs_on
mov %lcgs_onoff,1
return

*lcgs_off
mov %lcgs_onoff,0
return

*lcgs_flag
getparam i%rnd_result
mov %%rnd_result,%lcgs_onoff
return

*start

set_rnd_seed 1000
lcgs_on

*main
mov %50,10000
乱数を%50回発生させます。
mov %51,30
mov %52,80
理論上の最小値は%51、最大値は%52です。
rnd2 %0,%51,%52
mov %1,%0
mov %2,%0
for %3=2 to %50
rnd2 %0,%51,%52
if %0>%1 mov %1,%0
if %0<%2 mov %2,%0
next
実測最小値:%2
実測最大値:%1
click
lcgs_off %0
lcgs_flag %0
get_rnd_seed %0
get_first_seed %0
end

P.S.
2006-09-10 - Atelier de Muguet 制作記録で既に触れられていたんじゃよーぎゃわー!
いつだって俺は二番煎じだ。