みなさん、こんにちは。ぽけ太です。

今回は、以前から気になっていた「ペナントの『つづきから』を選択すると大きい乱数が約1万消費される現象」について、パワポケ11を用いた内部解析を行いました。

内容

始めに

実機で乱数調整をしていると、「つづきから」を経由した後だけ乱数位置が大きく飛ぶことがあります。

これまでは経験的に約1万程度進むことは把握していたものの、なぜこれほど大量に消費されるのか、また消費数が一定ではない理由までは分かっていませんでした。

今回melonDSのGDB機能とROMの逆アセンブルを利用して処理を追跡した結果、この約1万消費をPython上で再現できるところまで解析できました。

結論

結論から言うと、ロード時には、同じ種類の処理が12×12=144回実行されています。
1回あたりの処理は、おおよそ次の通りです。

  1. 固定で52回乱数を消費
  2. 26個の候補から10個を重複なしで選ぶ
  3. さらに一部の候補の状態値を乱数で更新する

特に重要なのが「26個から10個を重複なしで選ぶ」部分です。
同じ番号を引いた場合は再抽選になるため、必要な乱数消費数が毎回少し変わります。

つまり、「約1万」というのは固定値ではなく、初期seedによって変動する値です。

実際に解析した例では、

固定部分                 8352
重複なし抽選             1763
追加の状態更新            364
----------------------------
合計                    10479

でした。

数式にすると、

総消費数
= 144 × 58
+ 重複なし抽選で使った回数
+ rand(4)の結果の合計
+ rand(3)の結果の合計

となります。

さらに、この処理をPythonでそのまま再現しました。

DSの起動時間から初期seedを求め、

  1. DS起動
  2. ペナント「つづきから」
  3. 戻る
  4. 表サクセス
  5. オート命名
まで乱数を計算できます。

実機確認として、00:00:10 起動の場合を計算すると、オート命名は「原」になると予測されました。

実際にDSで同じ操作をすると、表示された名前も「原」でした。

つまり、ペナント再開時の約1万消費については、初期seedから正確な消費数を計算できるところまで再現できたことになります。

乱数生成の意味

ロード時に更新されているデータそのものの意味は完全には分かっていません。

解析上は、

  1. 0x308 byteのデータ × 12
  2. 各データ内に26個の要素
  3. 0~100程度の値を20/40ずつ変化

という構造が確認できています。

12は12球団、26は各球団の選手数、状態値は選手の調子に関係するものではないかと考えていますが、ここは現時点では推測です。

乱数調整という目的だけなら、今回の解析で十分です。

ロジックの例(Python)

今回のロジックをPythonで再現しました。

現在の大きい乱数の状態Xを受取り、ペナント続きから選択後の状態と消費回数を返す関数として表しています。実機での消費目安に組み込む際のご参考になれば幸いです。

A = 0x5D588B656C078965
C = 0x0000000000269EC3
MASK = 0xFFFFFFFFFFFFFFFF


def pennant_continue_state(x: int):
    """
    パワポケ11 ペナント「つづきから」時の
    大乱数消費を再現する。

    Parameters
    ----------
    x : int
        処理開始時の64bit大乱数state

    Returns
    -------
    after_state : int
        処理終了後の64bit大乱数state

    advances : int
        この処理で消費した大乱数の回数
    """

    x &= MASK
    advances = 0

    def next_rng():
        nonlocal x, advances

        x = (
            x * A + C
        ) & MASK

        advances += 1

        return x

    def rand(n):
        """
        0 ~ n-1 を返す範囲乱数。
        1回呼ぶたびに大乱数を1消費する。
        """

        state = next_rng()
        high32 = state >> 32

        return (
            high32 * n
        ) >> 32

    # 12 × 12 = 144 block
    for _ in range(144):

        # 26要素 × 2回
        # 固定52消費
        for _ in range(52):
            next_rng()

        # 26個から10個を重複なしで抽選
        # 重複すると再抽選されるため、
        # ここはseedによって消費数が変わる
        selected = set()

        while len(selected) < 10:
            selected.add(
                rand(26)
            )

        # 0~3
        q = rand(4)

        # 3~6回
        for _ in range(q + 3):
            rand(100)

        # 0~2
        t = rand(3)

        # 1~3回
        for _ in range(t + 1):
            rand(100)

    return x, advances

最後に


これまで「ペナントをロードすると約1万消費する」としか分からなかった部分を、初期seedごとに正確に計算できるようになりました。

ただし、今回はパワポケ11のみで確認した内容です。他シリーズでは未検証のため、この結果が適応されない可能性があることにご留意ください。

乱数調整の際、より効率的に目的の消費回数までたどり着くための参考になれば幸いです。