見出し画像

三国志NET(cron起動式CGIゲーム)の開発で乱数を使うことの難しさ

私の人生における長年の課題、未だ完全には解決できていない、それは三国志NETというcron起動型CGIゲームで乱数を偏りなくうまく扱うということ。
cron起動型という用語があるかは知らないが、要するに定期的に自動実行されるタイプのCGIゲームのことである。

記憶の連続性がない三国志NET

通常、ゲームといえばイベントドリブンなアプリケーションを想像することと思う。そこではゲームを始めて、終了ボタンを押すまで連続した時間や記憶が維持される。でも、CGIゲームにはこの連続性がない。

「博士が愛した数式」という小説がある。ネタバレにならない程度に紹介すると、交通事故の後遺症で記憶が80分しかもたなくなってしまった老博士の物語である。新しく覚えたことが80分ごとに記憶リセットされてしまうので、大事なことはメモに書いて携帯することで、大切な記憶を失わないように守っている。切ない。
三国志NETもこの老博士と同じように、実は一分に一回処理(コマンド実行とか)を終えたらきれいさっぱり記憶喪失する。なので外部データ(ファイルやDBなど)に大事なことをメモしておき、記憶喪失になったあとはメモを読み直すことであたかも記憶が連続しているかのように振る舞っている。切ない。

前提知識:三国志NETは一分に一回記憶喪失

そんな忘れん坊の三国志NET。一瞬で思いだして一瞬で忘れる、その繰り返しでもだいたいのことは何とかなる。何とかなるけれども、いかんともしがたいこともある。それが、本稿のテーマ「乱数」である。

どうすれば記憶喪失後にも乱数列を維持できるか?

三国志NETはひとたび処理が終わってしまえば、以前にどんな乱数が使われたのかは忘却の彼方である。1分に使った乱数を、2分や3分時点では覚えていない。じゃあ三国志NETが以前の乱数を思いだせるように外部にメモするか?そんなことができれば楽なのだけど、探せども探せどもperlのrandに対して乱数の種を直接指定する方法が分からない。私のプログラミング知識が足りていないためか、そもそもそんな機能は存在しないのか。もっといい乱数(Crypt::Random, Math::Random::Secure, Math::TrulyRandomなど?)を使えば、解決できる問題なのか。処理が重たくならないか、オーバースペックにならないか。そもそも、モジュールを変えたとて乱数の種を直接指定できるのか。この記事を書いたら、てんすいさんに見せてコメントをもらおうと思う。これを偶然見かけたプログラマ界隈の人にも教えてもらえたら嬉しい。

※補足:perlにsrand関数があることは認識しているのだけど、perl5.004以降では最初にrandが呼ばれたタイミングで内部的に乱数の種が設定されてしまうと思ってます。(その時にperlコンパイル時に生成されているRANDBITSを使うはず)ここにも私の誤解があるのかもしれない。srandに値指定で強制上書きできたりするのかな。

どうやって、乱数の不規則性を連続的に保っていくのか。
perlの標準乱数に頼らず、もっと柔軟に利用できる乱数モジュールを開拓するか、CGIの特性に合った乱数生成のモジュールを自作でもするしかないのか。最終手段は自作と思うけれどもなかなか踏み切れない。
だって、乱数生成器に手を出すなんて予測できない問題が起こりそうじゃん。乱数なんてゲームの基盤じゃん。大掛かりな改築工事になるじゃん。ちゃんと動くか怖いじゃん。なんか重たくなりそうじゃん。そこまでしてもやっぱり乱数が偏ったら怒られそうじゃん。俺の天完はお前の乱数のせいだってプンプンプンプンする人いるじゃん。なんでわざわざ乱数生成器を作るのかをトウゴスさんになんて説明するの。
シンプルなコードで計算負荷をかけずにやりたいじゃん。

できない理由を並べ立てるのではなく!
いや、できるだけ自作したくないでござる。うまくいかないことはみんなperlのせいにしたいじゃん!!!)^o^(

乱数とは何か、どう生成するか

そもそも「乱数」とは何か、乱数の種とは何か、その説明を少し書こうと思う。乱数列とは等確率に数字が現れる、規則性がない(ように見える)数列のことを指す。その数列から値を順番にとって使う。その値が乱数。

そもそも、コンピュータは不規則なふるまいをすることが苦手で、規則的にしか動けない。そんなコンピュータが不規則に見える乱数列を作るためには、この規則で数列を作ればいい具合に不規則になることが数学的に証明されているよという方法を使っている(perlのrandもそう)。いろんな種類があるけど、例えば、前に作った乱数の値に何か乗算して何か加算したあと剰余を取る線形合同法がある。(線形合同法 - Wikipedia

線形合同法の中で比較的よく使われるといわれる乱数生成器の例。具体的でわかりやすい

普通に数式で作ってるけど、これ乱数になるのかなって思うじゃん?
これすごいから。すごいばらけるから計算してみてよ。
すごすぎて、逆にランダムじゃないからこれ。0から4294967295までの数字が、一回ずつバラバラに出てくる。数学すごい。
※補足:ちなみに、上記乱数生成式はこのまま使うと偶数と奇数が交互に出てきてしまうので利用には注意が必要。(2^32が偶数なので)

こうやって数学的に確定的な方法で作られる乱数は疑似乱数と呼ばれる。疑似乱数の特徴は、再現性があること。
X1=1だったら、X2は必ず1,103,527,590になる。X3は必ず2,524,885,248になる。エクセルで計算したから間違ってたらゴメン。まあそんな感じで、初期値を指定したら乱数列がバシっと決まる。この初期値X1を「乱数の種」と呼ぶ。

私がやりたいこと

n分の更新の最後に作った乱数を覚えておいて、n+1分の最初に乱数の種として指定できたら、記憶喪失した三国志NETさんでも同じ乱数列を使い続けさせることができるじゃん。三国志NETはそういうことやりたいじゃん。やりたいなぁって。

理想の姿。次世代につながれるいのちのリレー

こうやって作られた疑似乱数は、円環の理のようにぐるぐると回り続ける。永遠にいい感じ。perlのrandがちゃんと周期最大なら永遠にハッピー。
余談だけど、私のような素人が適当に疑似乱数を作ると、周期がめっちゃ小さくなったり、円環ハッピー周期に入る前に、何かの数列が先頭に入ることもある。その部分はヒゲと呼ばれる。

疑似乱数のかたち。円環の周期が最大だとハッピー。ヒゲはなくていい

不規則じゃないといっても、実害なんてあるのか?

ねえ、気にしすぎなんじゃない?perlのrandで普通に毎回乱数作ったらいいじゃない。そんな風に思いたくて仕方がない、だけれど、己鯖であの禍々しい事件が起こったのは2023年2月のことだった。気づきの切欠となったのは、災害イベントの導入である。一日に1回くらい出るかなと思った災害イベントが、一日に4回も集中して発生し阿鼻叫喚の災害期となってしまった。

●【イベント】[1630年11月]大干ばつが起こりました!(2日23時20分)
●【イベント】[1630年06月]疫病が流行っているようです。街の人々も苦しんでいます。。(2日21時41分)
●【イベント】[1627年10月]疫病が流行っているようです。街の人々も苦しんでいます。。(2日11時0分)
●【イベント】[1627年05月]大地震がおこりました!(2日9時21分)

おのれさば2023年2月の事件

そんなにまとまって災害起きなくてもいいじょん。内政大変やん。
災害は全体に与える影響が大きく、しかもわりと高頻度で高密度に発生したので、乱数がいけてないんじゃないかと調査を開始。
災害イベント発生のフラグは、各月の最初に発生させる乱数で決まる。最初の乱数…嫌な予感がする。独立に作ってる乱数列の最初の値をとってきて、それを並べたらいい感じの乱数になるかって、なるわけがない。独立に作ってるんだから。

最初の乱数値を並べたらいい感じの乱数になるか?ならない

そして月初めの最初の乱数を毎回出力させてみたところ、次のような想像を超える怪しさの数列ができてしまった。
24, 43, 3, 22, 42, 53, 13, 32, 52, 11, 31, 42, 2, 21, 41, 0, …

何これ、うまく言えないけど気持ち悪い。
次に何がくるか微妙に予測がつきそう。何回もやってると、ものすごくヒゲが長くて、周期が小さいクソクソ過ぎる乱数列のように見えてきて頭を抱える。まさか規則性があるというのか。

そして災害発生月にパターンあるんじゃないかと言及され始める。
勘が良すぎるよ。さすまたん。

https://twitter.com/rarokka_high/status/1628289581140549633


なぜこんな禍々しい数列になるのか?perlの乱数の種生成方法に規則性があるんじゃないの?でも、私のローカル環境にインストールされたActivePerlではしつこく試してみてもそんな問題は再現されなかった。サーバ上のperlのRANDBITSが悪いならperlを再コンパイルすればいいのかもしれないけど、さくらインターネットさんはそんなことまでやってくれない。

結局、最終手段投入で災害イベントのためだけの特別な乱数生成器を自作してしまった。まあ災害イベントだけなら処理もそんな重たくならないでしょい。
いざ作ってみれば我が子のようにかわいい、災害イベント用の乱数は私のお気に入り。

そんなこんなで、現在の己鯖では用途別に効果や負荷を鑑みつつ、標準乱数のほかに2種類の乱数生成器が自作されているのである。



この記事が気に入ったらサポートをしてみませんか?