Try   HackMD

BDSPの乱数調整に関するメモ

はじめに

あけましておめでとうございます. ニアトです. 2022年もよろしくお願いいたします.

さて, BDSPが発売されてから一か月以上経ちますが, 剣盾にて証乱数が発見されたことによって乱数界隈ではダイパリメイクのことが忘れ去られつつあるように感じます.

このままPokémon Legendsが発売したら完全に忘れされられてしまいそうな気がしたので, BDSPに関連するお話を一つ書き留めておくことにします.

BDSPの乱数生成アルゴリズムについて

ポケモンBDSPでは, GlobalRNGとLocalRNGの2つの乱数生成器が存在しており[1] [2], 基本的にはGlobalRNGを用いてランダムな事象を決定しています. [3]

LocalRNGには, 剣盾でも用いられているXoroshiro128+が使われており, 剣盾にて先駆者がいくつかの調査がなされていることから, 今回の話ではGlobalRNGに用いられているXorshift128について取り上げることにします.

Xorshift128について, 数学的な説明をざっくりとしておきます.
Xorshift128は, 4つの32bit変数

\boldsymbolxn,\boldsymbolyn,\boldsymbolzn,\boldsymbolwnF232からなる, 計128bitの内部状態を持つ乱数生成器です.
ある時点での内部状態を並べて,
\boldsymbolrn=(\boldsymbolxn,\boldsymbolyn,\boldsymbolzn,\boldsymbolwn)F2128
と表したとき,
\boldsymbolrn
\boldsymbolrn+1
に更新する遷移行列
T
が存在します. (言い換えれば,
T
\boldsymbolrn+1=T\boldsymbolrn
を満たす行列になります.)

具体的な

Tの形は, Xorshift128の遷移行列とその逆行列を考える#遷移行列の生成をご覧いただければと思います.

また, 出力は内部状態の下位32bit, 即ち

\boldsymbolwnとなります.

手法の吟味

乱数調整をする際に用いられる手法は2通り存在します.
1つは乱数生成器に与えられる初期状態(seed)を調整してから目的の内部状態まで遷移させる手法で, もう1つはある時点での内部状態を出力から逆算してから目的の内部状態まで遷移させる手法です.

近年(第六世代以降)では, 初期状態を人為的に操作することが困難なことから, 後者の手法が採られることが多いですね.

今回についても, 後者の手法を採用します. 内部状態の復元手法についてですが, 実のところSM, USUMの孵化乱数(TinyMT)[4]や剣盾の証乱数(Xoroshiro128+)[5]と似たような手法が使えます. 同じ方法が使えるならそれで殴れば良いじゃないかということで具体的に殴っていきましょう.

Xorshiftの内部状態の復元(1)

乱数生成器の内部状態を復元するには, その出力を十分な量だけ観測して, 内部状態を逆算してやる必要があります. ゲーム中にGlobalRNGが使われている場面は数多くありますが, 今回はフィールド上での主人公の瞬きに注目します.

主人公の瞬きを決定するために, 約1秒に1回の間隔で乱数消費が行われます. このときの乱数生成器の出力

wによって, 瞬きの種類が決定します. これは次のような関数
f(w)
で表すことができます.[6]
f(w)={single(w&0xF=0x0)double(w&0xF=0x1)nothing(otherwise)

ここで, 演算子

&はbitごとのAND演算を表すものとし,
single
は1回瞬き,
double
は2回瞬き,
nothing
は瞬きをしないことを表すものとします.

つまりこの関数は, 主人公が瞬きをしたときの乱数生成器の出力の下位4bitが観測できることを意味しています.

補足:

「瞬きが発生した場合は、瞬きの種類に従って、乱数値の下位4bitが0000か0001であることが確定する」ということですね。
(夜綱氏によるコメント)

しかしながら, 瞬きをしていない時の出力からは殆ど情報を得ることが出来ないため, これまでのような "連続する

n個の乱数列の下位
k
ビットの出力から逆算する" 手法をそのまま使うのは難しそうです.

Xorshiftの内部状態の復元(2)

主人公の瞬き以外に乱数を要する要素が存在しないことを仮定します.

先ほどの話の通り, 瞬きから連続する乱数列の情報を得ることは困難ですが, 主人公が瞬きをしたある時点を基点として, 何消費目に瞬きが生じる出力となったかを観測することは出来ます.

基点から瞬きが起こった時点までの経過秒数がほぼそのまま消費数となることを利用しましょう.[7]

考察に移ります.

瞬きの種類

single,doubleに対して, それぞれ
0,1
が割り当てられているものとします.

最初の瞬きを基点として, 発生した

n回の瞬きに対して,
k
回目の瞬き
bk{0,1}
が発生した時の消費数(=経過秒数)を
dk
(但し,
d1=0
)とします. このとき, 瞬きから得られる
4n
bitの情報を並べることでできる瞬きベクトル
\boldsymbolbF24n
を次のように定めます.
\boldsymbolb=(0,0,0,b1,0,0,0,b2,,0,0,0,bn)

基点でのxorshiftの内部状態

\boldsymbolrF2128から
\boldsymbolb
を計算する
4n×128
行列
S
を考えます. (このときの行列は正方行列とは限らないことに注意してください.)

そのような行列

Sは, 遷移行列
T
dk
乗したあとの第
i
行目の成分
(Tdk)i
を用いて次のように表すことができます.
S\boldsymbolr=\boldsymbolb((Td1)125(Td1)128(Td2)125(Td2)128(Tdn)128)\boldsymbolr=(0b10b2bn)

あとは,
F2
上の計算であることに注意しつつ,
4n
本からなる128元一次連立方程式とみなしてGauss-Jordanの消去法を適用してやることでxorshiftの内部状態
\boldsymbolr
を復元することが出来ます.

余談

行列

Sを正方行列と限定しないのは, 行列
S
が正方行列だとしても必ずしも正則になるとは限らないためです. 実用上は32個より多くの瞬きを観測したうえで行列
S
を生成し,
rank(S)=128
となるようにするべきでしょう.

また, この記事を記述する際に, 夜綱氏にお力添えをいただきました. この場をお借りして御礼申し上げます.


  1. https://twitter.com/SciresM/status/1461396129463947265 ↩︎

  2. 厳密には日替わりの事象を決定する乱数生成器も存在する(https://bzl.hatenablog.com/entry/2021/12/03/002525)ため3つです ↩︎

  3. https://twitter.com/SciresM/status/1461396133079441408 ↩︎

  4. https://xxsakixx.com/archives/55570518.html ↩︎

  5. https://hackmd.io/@yatsuna827/ByqFSb6KK ↩︎

  6. https://github.com/zaksabeast/BDSP-RNG-Reference/blob/main/src/timeline.rs ↩︎

  7. 実際は1秒よりも僅かに長いようです ↩︎