dq9 解析メモ(daisukedaisukeのブログ)

2023年10月から2024年6月まで、約9カ月間のdq9解析結果を投稿します。エンカ調査がメインです。

dq9のB、C乱数を等価変形する

この記事はAIが書きました。他の記事は人間が書いてます

dq9 における B 乱数および C 乱数は、下記のとおりである。

dq9 のパーセント計算に用いられる乱数は、以前の記事で述べた通り、比較的複雑な式によって構成されている。
本稿はあくまで備忘録であり、記述が簡潔すぎる点については了承されたい。

qiita.com

このような実装になっている理由は、dq9 が 32bit 環境を前提として設計されたゲームであることに起因すると考えられる。
一方で、現在主流の 64bit 環境においては、このパーセント処理は等価変形によって十分に再現可能である。
ゲーム内では、[0,1]乱数がクッションになる形で動作しているが、64bit環境では等価変形により乱数から直接パーセントを求めることができる。

以下では、top32 を乱数の上位 32bit、すなわち (乱数 >> 32) と定義する。

getFloatRand

すべての乱数の基礎となる [0,1) のパーセント値は、次の式で算出される。

(Top32 * 100 / 2^32) * 0.01

この式では、*100*0.01 が互いに打ち消し合うため、次のように簡約できる。

= Top32 / 2^32

さらに、除算を定数倍に変換すると、

= Top32 * (1.0 / 2^32)

という形で表現できる。

ゲーム内部でのgetFloatRand

一方、ゲーム内部の 020754d8 (getFloatRand) では、次のような処理が行われている。
static_castとはc++に存在する処理であり、データ型の変換を行うという指示である。
ただし、変換先の型が持つ表現精度を超える場合に丸めが発生し、精度の低下が起こり得る。

floatRand = static_cast<float>(
    static_cast<double>(top32) /
    static_cast<double>(4294967295)
)

これは [0,1] の範囲を取る乱数である。
分母が 4294967296 (232) ではなく 4294967295 (232−1) になっている点については、初期解析時の重大な誤りに起因するものであり、長年にわたって実害がなかったため修正されずに残っているものと考えられる。

この式も同様に、除算を定数倍に変換すれば、

top32 * (1.0 / 4294967295.0)

と表せる。

シフト演算にできない理由

= Top32 * (1.0 / 2^32)

この式は、数学的には

Top32 / 2^32

と等価であるが、この形のままではシフト演算に置き換えることはできない。

理由は、結果が整数ではなく、0〜1 の小数値になるためである。


右シフト演算 >> 32 は、整数値に対して

x >> 32  ==  floor(x / 2^32)

という意味を持つ。 したがって、

Top32 >> 32

は常に 0 となり、元の意味を失う。


一方で、

top32 * max >> 32

が成立するのは、あらかじめ max を掛けることで値域を整数側に拡張しているためである。

(top32 * max) / 2^32

は、max が十分大きい整数であれば、 右シフトによる整数除算が意味のある結果を持つ。

RandInt

また、ゲーム内の 02075488 (RandInt) では、

floatRand * max

という形で [0, max-1] の乱数を生成している。


この処理も 64bit 環境では等価変形可能であり、整数演算に落とすことで、

top32 * max >> 32

と変換できる。

これは、元の処理が

floatRand * max

であり、floatRand が実質的に

top32 / 2^32

として振る舞っているためである。

すなわち、

floatRand * max
≈ (top32 / 2^32) * max

となり、分母の 2^32 は 64bit 整数において右シフト 32bit と等価である。 そのため、

(top32 * max) / 2^32

を、

top32 * max >> 32

として計算できる。

この方法では、浮動小数点演算による丸めを介さず、 元の処理とほぼ同一の分布を整数演算のみで再現できるため、 高速かつ再現性の高い乱数生成が可能となる。
※AIに騙されて気が付きませんでしたが、この手法は最大1 / (232)の誤差が発生する近似です。

getFloatRandRange

また、指定した範囲の乱数を出力する getFloatRandRange (02075514) においては、
次の式によって値が算出されている。

min + floatRand * (max - min)

ここで用いられる floatRand は前述の通り等価変形が可能であるため、この処理全体も高速化できる。
具体的には、floatRandtop32 による正規化として扱うことで、

min + (top32 * (1.0 / 4294967295.0)) * (max - min)

と書き換えることができる。
これにより、浮動小数点演算の構造を保ったまま、元のゲーム挙動に十分近い値を高速に生成できる。

RandRangeInt

一方で、指定した範囲の整数乱数を出力する RandRangeInt は、

min + getPercent(position, max - min + 1)

という構成になっている。

この処理では getPercent が整数値を直接生成する設計であり、
浮動小数点による等価変形を挟む余地がないため、
getFloatRandRange のような等価変形的高速化は不可能である。

記事一覧

daisukedaisuke.hatenablog.com