再現可能な乱数発生器
真の低脳は意外なところに潜む - うさだ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命令を上書きして、数値の再現が可能な乱数を発生させることができる。
ただし、そのためにはいくつか準備命令が必要。
- set_rnd_seed命令で乱数の種を仕込む。(ここで与える数値は0以上lcgs_mの値未満でなければならない。)
- その後、lcgs_on命令を実行する。(引数はなし)これによって、今後rnd2を実行した場合は、このライブラリで追加した乱数発生器を使うので、再現可能性が保証される。
- lcgs_off命令でNScripter本来のrnd命令とrnd2命令に戻る。
- 今現在、どちらのモードになっているかは、lcgs_flag %0で取得できる。0が本来のモード、1が新規追加したモードである。
- 最初に与えた乱数の種が何であるかは、get_first_seedで取得できる。
- 現在の種、つまり直前に発生された乱数は、get_rnd_seedで取得できる。ただし、この数値は実際に発生させた値ではなく、変換前の乱数であり(初期設定のままならば)0から65535までのどれかの値を取る。
- アルゴリズムの細かい点を変更したければ、線形合同法 - Wikipedia読んでいじれ!
- なお、発生させられる乱数の最大値(rnd2にあっては、乱数の幅)は、最大でlcgs_mの2分の1になる。なので、現状では0〜32767までの乱数を発生させられることになる。(これは、線形合同法の弱点である最下位ビットを切り捨てるため)これ以上の幅が欲しければ、自分でlcgs_a, lcgs_b, lcgs_mの値を調整すること。
- 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 制作記録で既に触れられていたんじゃよーぎゃわー!
いつだって俺は二番煎じだ。