見出し画像

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

前回記事は内容が若干ニッチ(令和の世に、PerlでCGIでしかもcron起動のゲーム特有の乱数)で誰にも刺さらないことを覚悟していたけど、意外にも目を通してくれる人がいて有益なコメントを頂くことができた。というより、界隈の人たちの中に有識者が多くいた。勉強になることが多かったので、それをまとめるための続編を書くことにする。

結論:自作するしかない

結論から言うと、自作するしかなさそう。
そもそも、いまrandで使っている乱数列に周期があるという前提から見直しが必要。

標準乱数で実現する方法には無理がある
標準モジュールrand/srandを使ってプロセス間で同じ乱数列を引き継がせることは難しい。ゲーム期間中ずっと乱数の種を固定して、これまでの乱数実行回数を外部に記憶しておいて、各プロセスの最初に帳尻を合わせるためだけにその回数分だけrandを空実行すればできなくはないけど、負荷がかかり過ぎるので現実的に不可能。これをすると多分、どこかのタイミングでサーバが止まる。

強引にrandを空実行し続ける方法。
帳尻合わせの処理が数十~百万ループくらいになったら重たくなってサーバが止まる予感

そもそもrandの中身が未知だから、漸化式に直接値を入れるのは無理
srandで漸化式に直接値を入れ込む、そんなことができるわけ…いや、やってみればいいじゃない。ローカル環境でやってみたら駄目でした。
randの乱数生成方法の基本路線がXn+1 = f(Xn)の漸化式だったとしても、srandで指定した乱数の種がそのままX0として扱われるかどうかの保証はない。実際、そのような作りにはなっていなかった。
可能性低いけどrandの中身がもしかメルセンヌツイスター法だったら、漸化式はXn+2=f(Xn+1,Xn)みたいな形だろうし、中身がブラックボックスな乱数生成器に対して状態引継ぎなんてそんなシンプルにできるわけない。
もし私が世の中に自作の乱数生成器を公開しようと考えた場合、プロセスが死んだあとでも状態引継ぎができるように作るのか?
セキュリティ上の理由から答えはノーです、Sir!

標準乱数をどうにかする方法はナシ、しょうがない。

他の乱数モジュールでもできなさそう
そして、もっと作り込んである他の乱数モジュールならどうかとCPANのマニュアル(こことか…Crypt::Random - Cryptographically Secure, True Random Number Generator. - metacpan.org Crypt::Random::Seed - Simple method to get strong randomness - metacpan.org)を漁ってみるも、それっぽいものは見つけられず。
そもそも、乱数列を引継ぐとかそんなバカなこと考えず、普通に新しい乱数の種を使って、新しい乱数と新しい気持ちで生きていけよと、それでも大丈夫なくらいいい乱数の種作ってやるよと、そんなメッセージが書いてある気がした。

そんなこんなで、コーディングで煮詰まったとき、頼りになる人が近くにいることはなんて素晴らしいことなんだろう。chatGPTの一番の価値ってそういうとこなんですよね。AIだけじゃない、私には人間のアドバイザー てんすいさんもいる。幸せ噛みしめちゃうよね。
てんすいさん曰く「やっぱり自作するしか、ないかもですね♨」

実現方法

さて、やりたいことを実現するための現実的な方法は次の二つのようです。
①自作で乱数生成器を作る
②自作で乱数表を作っておき、参照して使う

①自作で乱数生成器を作る方法

シンプルな手法とシンプルな漸化式を選べば計算負荷は大きくない。
分かりやすいのは前回紹介した線形合同法、大きなメモリも消費しなくて済むので一番現実的。
Perlのrandもさくらインターネットさんが特別なこだわりでコンパイルをしていなければ中身は線形合同法と思われる。これからもPerlのrandを使い続ける身として、ここで正しい使い方を根本から理解しておくのは悪くない。
逆に言えば、適切な使い方をしないと事件が起こってしまう。
もしかして、天完が続くのってこの線形合同法の特徴なのかもしれないなぁ…なんて。

たとえば前回紹介した線形合同法の漸化式、あれは偶数で剰余を取っているので、そのまま使うと奇数と偶数が交互に出力されてしまう。ゲームバランスがあぶない。
そもそも線形合同法で生成された乱数の下位ビットはランダム性が低いと言われている、ゲームで使うためには下位ビットを切り取るなどの工夫が必要になるはず。
我々は怠っていたのではないか、料理でも大切な下ごしらえというやつを。マズ飯作っていたんじゃないのかと。

いや、待って。perlのrandの中身はまだ分からないから。
ブラックボックスだから。早まらないで!線形合同法でほぼ確だけど。
もしかしたら見えないところで下処理済かもしれないじょん。知らんけど。

とはいえ標準乱数randを使うときの下位ビット処理は割とすぐできそうな気がするから、問題のなさそうなところからちょっとずつ試すのはいけるかも。
16ビットくらいガツンと削っとけばいけるか。
$nice_random_value = ($random_value >> 16); こんな感じか。
えっ、そんなことしたら65536くらいまでしか乱数の最大値設定できなくなったりしない?大丈夫なん?まあ、大丈夫か。
え、実際に使うにはrand(6553600+$MAX) >> 16みたいにするの?
まあ、ええわ。ローカル環境でちょっと試してみればわかるじゃろ。

禍々しい乱数列(rand(6553600+100) >> 16)左が10進、括弧内は2進数

ちょっと待って。
どうして、下ごしらえしたらこんな飯マズ乱数になるのかな。

 U穴「この乱数、なにか変…」
 K原さん「何か気づかれましたか」
 U穴「この乱数を並べ替えると1145141919…暗号になっているんです」
なってないよ!
88とか48とかすぐ出てきてるし、下一桁が4,8の割合多くて気持ち悪いよ。

何回も試してみるとそんなに悪くない乱数列が出てきたので、最初のが特殊だったのかもしれないけど、プロセス間での引継ぎどころじゃなくなってきた感がすごい。こんな呪われた乱数列を引き継いだところで一体何の意味が。意味のない引継ぎだよ。

そもそもね、そもそもの大前提に立ち返ってみようよ。
perlのrandは普通の使い方をすると周期最適な疑似乱数であるという愚かな思い込み。その幻想から解き放たれて、いま旅立ちの時がやってきているのかもしれない。

初心に戻って100個のint(rand(100))
そうだよ、これ小数点以下入ってるから整数にまるめたら被るの出てくるよ

そもそもね、「randの下位ビットが~」とか言う前にね、「100以下の乱数を出してきて」なんて注文をした時点で、なんか加工されてるよね。割ってるんじゃないの?100で。そこでもう、周期という概念はなくなってしまうだろうに私ときたら…。

ゲームでよく使われそうな、0~99の整数乱数。これの周期最適版疑似乱数だけ自作して、部分的に使ってみたらどうだろうね。現実的な第一歩はそこからなんじゃないかな。

いや、もう漸化式で乱数を作ろうだなんて、そもそもそんなことに手を染めてはいけないのかもしれない。

「漸化式で乱数を作るのはある種の罪である」

フォン・ノイマン

②自作で乱数表を作る方法

乱数表、それは運命。これは自由自在に理想のバラつきを実現できるので限定的に使うならとてもアリ。だけれど、乱数表データは巨大になる。対象をかなり絞って、限定しないとメモリ管理とかその周辺で禍々しいことが起こる気がする。
例えば、己鯖は120年間(1600~1720年)=28,800分、一更新に一つの乱数使うだけでも28,800の乱数が必要になる。
例えば、戦闘中の計略・武AT・兵ATの発動チェックにだけ限定して使うとして、一戦だけで2人×3回×50ターンで最大300個の乱数が必要になる。1分に一戦しかなかったとしても(実際はもっと多いけど)、最終的には864万個の乱数を使うわけなので、多分、うまく処理しきれなくて止まる気がする。
なので、現実的に処理しきれる範囲の小さな乱数表をぐるぐると使いまわす形になると思うけど、乱数のバラつきが保てるならアリ。

それをしたら、どんないいことが?

ひょっとしたら嬉しいのは私だけで、自己満足のためかもしれない。
世界が美しいと私は嬉しい。空が青く、飛行機雲の白いライン、木々は風に揺られて、乱数は最適周期の軌道に乗っている。それが嬉しい。

そんなポエムでいったい誰が納得するというのか。

幸運度に偏りがあるのが嫌だという人は多い。
もしもその偏りを小さくできたら、それは価値なのではないかなぁ。
でも、なんだろう。幸せってなんだろう。

対人戦で発動率2%の武ATが出たときはたぶん幸せじゃない(数値的にはすごく幸運)。その相手は相対的に不幸とされる。それは本当に不幸なのかという問題はある。
通常計略は発生可能なターンがたくさんあるけど、先制やカウンターはそもそも分母が小さい。単純比較していいのかという問題もある。
どういうことかというと、例えば極端な例だけど、勝率ランキングで1戦して勝率100%の人と、50戦して勝率70%の人がいたとして、この100%と70%を同列で比較するんですかという問題に近い気がする。(まあ、それを言い出したらキリはない)

だけど、ポイントはそこじゃない、幸運度の数値じゃない気がする。
相手だけ毎ターン特殊アタックを繰り出してくるのに、自分は発動率30%のアタックが一度も出ないことが不満の原因なんじゃなかろうか。しかもそれが10戦近く続いたら?私は10連続天完だったことがある。さすがにきつくて、よく覚えている。そういうストレスフルな闘いの数が減ったらどうか。もしも減らせるとしたら。(ちなみに減る可能性はある気がするけど、減る保証はない)

そもそも、乱数がバラついたらゲームは面白くなるのか。他のゲームでも、たとえば麻雀は配牌やツモの良さが横並びなら面白いのか。
偏った方がドラマが起こったり面白いこともあるんじゃないか。守備で5枚止めたぞーうおーみたいな。少なくとも私の10連続天完は、思い出として記憶に残っている。だから、あんまり極端なことにならない程度に調整できたらいいなと思う。

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