Try   HackMD

BDSPのID調整の原理に関してのメモ

はじめに

ご無沙汰しております. ニアトです.

ダイパリメイクことブリリアントダイヤモンド・シャイニングパール(以下BDSP)は, 既に忘れ去られたゲームとなりつつありました.

しかし, オーキドのてがみ配布やてんかいのふえの実装, 更には某掲示板の乱数調整スレにBDSPにおけるID調整手法が書き込まれたことにより, 再度日の目を見ようとしています.

せっかくの機会ですから, BDSPにおけるID調整手法の原理を書き残しておくことにします.

注意:
この記事は具体的な乱数調整手順/ツールの使い方/etc..の解説は一切行いません.

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

改めて, BDSPの乱数生成アルゴリズムを確認しておきましょう. とはいえBDSPの乱数調整に関するメモでも解説はしているので, 必要最低限の部分だけ確認しておきます.

BDSPにおけるランダムな要素(ex. 野生ポケモンの種類, 性格, 個体値, etc)を決定するのには, Xorshift128が使われており, Unityの標準乱数生成アルゴリズムがそのまま流用されています.

トレーナーカードに表示されるIDについても, ここから取ってきた値に基づいて決定されています.

準備(1)

実質的なトレーナーIDの決定タイミング[1]は, 主人公の顔写真を選択した時です. (下図参照)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

従って, ゲーム開始時から顔写真選択までの乱数契機から, 乱数生成器の内部状態を復元しなければなりません.

その区間で乱数を消費している観測可能なオブジェクトといえば

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

ゴンベ「ごんぬ

実はゴンベの瞬きタイミングの決定に乱数が用いられており, 乱数消費を起こすオブジェクトは存在しません. つまり, ID決定タイミングまでにゴンベ以外の乱数契機は存在しません.

ゴンベが瞬きしたとき, 乱数生成器の出力

rを用いて, 次の瞬きまでの待機時間
t
[sec]を決定しているのです.

瞬き間隔を調査したところ, その間隔は概ね3~12秒, 言い換えれば

t
[3,12]
に含まれていることが分かりました. また, この待機時間は整数ではなく浮動小数によって決定されています.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

主人公の瞬きの場合とは異なり, Xorshiftの出力値に対して剰余をとって計算されているようでもありませんでした. 恐らく, Unityの標準乱数ライブラリの何らかのメソッドを呼び出して計算しているものと思われます.

そんなわけで標準ライブラリの仕様を調べてみると, Andante氏が仕様を詳細にまとめてくださっていました. [2]

Andante氏の調査結果に基づくと, intervalの決定は, rを乱数生成器の出力として次のような処理になっていると考えられます.[3]

float tmp = (r&0x7fffff) / 8388607.0
float interval = 3*tmp + 12.0*(1.0 - tmp)

この処理を

f(\boldsymbolr)=tと表すことにします.

また, 出力された結果intervalを基に, 元の乱数生成器の出力の下位23bitを逆算することもできます.

具体的には, 観測された瞬き間隔をinterval, 逆算結果をr_として, 次のようになります. (Andante氏による逆算式を用いました)

float norm_f = (12.0-interval)/(12.0-3.0)
float norm_i = (int)(norm_f*8388607.0)
r_ = norm_i & 0x7FFFFF

同様に,この逆算処理を

f1(t)=\boldsymbolrと表すことにしましょう.[4]

手順としては, intervalの計算式をtについて解いて, 8388607.0を掛けてマスクし直しただけです.

intervalの計算式に除算が含まれている都合上, 復元して得られる結果には微小な計算誤差が生じますが, 下位数bitにしか影響しないことと, 後述する問題点を踏まえれば些細な問題でしょう.

取り組むべき問題

どうやらゴンベの瞬き間隔からXorshiftの出力が得られそうなことは分かりました.
しかしながら, 肝心の瞬き間隔の観測はどのように行えばよいでしょうか?

前回の主人公の瞬きの場合は約1秒ごとに判定があることが分かっていましたし, 観測精度についてもそこまでの厳密性は必要ありませんでした.

しかしながら, 今回の場合はいつ次の瞬きが生じるかが予測できませんし, そもそも瞬き間隔そのものが逆算における重要なファクターとなっています.

なるべく計測誤差を抑えて観測を行いたいのですが, 諸々の観点から誤差を0にすることは困難です.

そこで, 今回は計測誤差についてアレコレするのは諦めて, 逆算手法を工夫することで対処します.

準備(2)

乱数生成器の出力を次のような形で表すことにします.

\boldsymbols=s32s31s2s1

あるタイミングで得られるゴンベの瞬きの間隔(瞬き間隔の秒数)について, その真の値[5]

dとおきます.

真の値

dから逆算して得られる, 乱数生成器の出力の下位23bitを
\boldsymbold
とすると,
\boldsymbold=000d23d22d23kd2d1

となります.

乱数生成器の内部状態の復元には

\boldsymboldを直接使うことは出来ないため, 代わりに計測によって得られる観測値
s
を用いる方法を考えましょう.

但し, 真の値と観測値の誤差が高々

εで抑えられること, 即ち
|ds|ε
を満たすことを仮定します.

sに対して同様にして逆算を行えば,
\boldsymbold
の推定値として
\boldsymbols¯
が計算できます.

\boldsymbols¯=000s23s22s23ks2s1

さて, 内部状態の復元に下位

23bitから
23k
bitの部分を用いるのであれば,
d23d22d23k=s23s22s23k

を満たす必要があるのは明らかです.

しかしながら, 誤差

εを踏まえると, この誤差による繰り上がり/繰り下がりが生じることで当該bitまで伝播してしまう場合があります.

具体的には, 区間

Gi=[f1(i×223k)ε,f1(i×223k)+ε]
(i=0,1,2,2k)
に含まれる観測値
s
に関しては, 所与の等式を満たさない可能性があります.

k=4, 即ち下位23~20bit(
r23r22r21r20
)を用いる場合, 繰り上がり繰り下がりの範囲を数直線上で図示すると以下のようになります.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

下位23~20bitの部分が変化する境界値を中心とする前後

ε秒が該当区間である, と言うとピンとくるかもしれませんね.

数直線の上部の値は観測値

sが上記の区間に含まれる場合は復元に失敗する恐れがあるため, そのような観測値を無視する必要が生じます.

Xorshiftの内部状態の復元

取り合えずここまでの話を纏めると,

  • ゴンベの瞬き間隔から乱数生成器の出力を部分的に逆算できる
  • 但し, 観測精度を考慮するとXoroshiroの内部状態の復元に利用できない瞬きが存在する

ということが分かりました. これらを踏まえて, Xorshiftの内部状態の復元手法を考察します.

前提条件として, 瞬き間隔の誤差が高々

εで, 乱数生成器の出力の推定値のうち下位
23
bitから
23k
bitの計
k
bitを用いることを仮定しましょう.

n個の連続する瞬き間隔を
Dn={d1,d2,dn}
, それらから逆算することで得られる乱数生成器の出力の推定値を
S¯n={\boldsymbols¯1,\boldsymbols¯2,,\boldsymbols¯n}={f1(d1),f1(d2),,f1(dn)}
とおきます.

Dnから, 無視しなければならない瞬き間隔を取り除いて得られる,
m
個の要素からなる部分列
D
を求めましょう.

区間

Giを次のように定義します.

Gi=[f1(i×223k)ε,f1(i×223k)+ε]  (i=0,1,2,2k1)

Giから定義される添え字列
{am}={j|i,djGi}
を用いることで, 部分列
D
は次のように構成されます.
D={da1,da2,,dam}

S¯nも同様にして部分列
S¯
を求めることができます. すなわち,
S¯={\boldsymbols¯a1,\boldsymbols¯a2,,\boldsymbols¯am}={f1(da1),f1(da2),,f1(dam)}

です.

回りくどい書き方となりましたが, 要するに

S¯の意味するところは"復号に利用できない値を取り除いて得られる乱数生成器の出力(の推定値)"となります.

S¯を求めることが出来れば, あとは以前の記事の手法(Xorshiftの内部状態の復元(2))と同様です.

S¯の各値を二進数表記に直して, 下位
23
bitから
23k
bitまでを一列に並べることで得られる,
km
次元上のベクトル
\boldsymbolbF2km
を定めて,
\boldsymbolb=((s¯a1)23,(s¯a1)22,(s¯a1)23k,(s¯a2)23,,(s¯am)23k)

Xorshiftの遷移行列

Tを用いて, 内部状態
\boldsymbolrF2128
から
\boldsymbolb
を計算する
km×128
行列
U
を次のように構成し,
U\boldsymbolr=\boldsymbolb((Ta1)12823(Ta1)128(23k)(Ta2)12823(Ta2)128(23k)(Tam)128(23k))\boldsymbolr=((s¯a1)23(s¯a1)23k(s¯a2)23(s¯a2)23k(s¯am)23k)

(遷移行列

T
ai
乗したあとの第
j
行成分を
(Tai)j
と表しています. )

得られた

km本からなる
F2
上の連立方程式を解くことでXorshiftの内部状態
\boldsymbolr
が復元できますね.

実装について

ゴンベの瞬きを用いたXorshiftの内部状態の復元の実装は, この手法に基づいたものとなっています.

各種パラメータについては,

k=4(一つの瞬き間隔から得るbit数),
ε=0.1
(計測誤差),
n=64
(瞬きの観測数),
m=36
(実際に復元に用いる瞬きの数)としています.

余談

残念ながらこれらのパラメータについては議論の余地があります.

k,ε の値は環境によって大きく異なるでしょうし, 少なくとも
m
の値は決め打ちにするのは望ましく無さそうです[6].


  1. 本来のID決定タイミングは名前入力完了時です. しかしながら, 主人公の顔写真を選択した時点で乱数消費が止まり, 主人公の名前入力の完了まで消費が生じないことから, 顔写真選択時が事実上のID決定タイミングと見なすことができます ↩︎

  2. Andante. “UnityEngine.Random の実装と性質”. 屋根裏工房改. 2020/12/01.
    https://andantesoft.hatenablog.com/entry/2020/12/01/225931, (2022/03/14閲覧) ↩︎

  3. 出力の結果と, 文献に記載されているRandomクラスの実装を鑑みるに, 恐らくRandom.Rangeメソッドが呼ばれていると思われます. ↩︎

  4. 厳密には逆関数ではないのですが, 逆算の意味を分かりやすくするためにこのような表記をとりました. ↩︎

  5. 乱数生成器の出力に基づいて計算される値のこと ↩︎

  6. 実は復元に用いるための瞬きが十分な数得られなかったことがindex errorが送出される原因にもなっています. 観測数を増やすことで解決できるかもしれません. ↩︎