見出し画像

【スプラ3】追加ギアパワーはseed値からイカにして計算されるのか

この記事は、Pokémon Past Generation Advent Calendar 2022 12月24日の記事です。(ポケモンなのにスプラの記事書いてるが…乱数関連なので許して)

およそ2か月半前、Leanがリリースしたgear seed checkerを使ったツイートがバズり、様々な意見が飛び交いました(中には改造だBANだという声もありましたが)。

実際、今までSplatoonシリーズはこういったseed値を特定したり乱数調整(またはそれに近しい行為)に触れてこなかった層がほとんどですので、いきなりそれが出てくれば忌避感を抱くのも無理はありません。
(又聞きな話ですが、3でこういったツールができたのは任天堂の改造対策が功を奏している≒海外勢が「下手にギアを改造するよりseed値を特定して正規の範囲でギアを作るほうが賢い」と判断したという話もあります。)
ですので今回の記事は、ツール(そしてスプラ3)が追加ギアパワーをイカにして計算しているのかを実際にpythonで書いてみたいと思います。

完全なランダムなんてない

今のゲーム…もとい今あるコンピューターは疑似乱数で「ランダムっぽい結果」を生成しています。
そしてその擬似乱数を生成する際に種となる数値が、seed値です。
(ギアだけでなく、ガチャ、サモランの報酬、スパイキーの注文、ブキチドローンなどランダムっぽい結果が求められるものには全てseed値があります。)
seed値と生成方法が同じであれば、コンピューターは同じ結果を出力します。(おや…?この現象どこかのポケモンで最近見ましたね…?)
つまり生成方法が判明すれば、(場合によるが)結果から逆算することができるわけです。
海外勢はスプラ3を解析し「ギアごとに32bitのseed値があり、アルゴリズムとしてxorshift32が使われている」事を突き止めました。

コード全文

まずコード全文をここに書いておきます。
argparseでseed値やブランド,ドリンクの有無,結果の表示数などをコマンドライン引数として取り込んでいます。

import argparse

parser = argparse.ArgumentParser(description='seedからこの先に付くギアパワーを計算し表示する')
parser.add_argument('seed', type=lambda hx: int(hx, 0), help='16進数のseed(先頭は0xを付ける)')
parser.add_argument('brand', type=str, help='ギアのブランド名')
parser.add_argument('--drink', type=str, help='ドリンクで付きやすくなっているギアパワーを指定する(無い場合は"なし")', default='なし')
parser.add_argument('--display', type=int, help='結果の表示数')
args = parser.parse_args()

seed = args.seed

ability_order = [
    "インク効率アップ(メイン)",
    "インク効率アップ(サブ)",
    "インク回復力アップ",
    "ヒト移動速度アップ",
    "イカダッシュ速度アップ",
    "スペシャル増加量アップ",
    "スペシャル減少量ダウン",
    "スペシャル性能アップ",
    "復活時間短縮",
    "スーパージャンプ時間短縮",
    "サブ性能アップ",
    "相手インク影響軽減",
    "サブ影響軽減",
    "アクション強化"
]

brand_order = {
    "バトロイカ": [11, 0],
    "アイロニック": [9, 8],
    "クラーゲス": [4, 12],
    "ロッケンベルグ": [3, 4],
    "エゾッコ": [6, 5],
    "フォーリマ": [7, 1],
    "ホッコリー": [1, 2],
    "ホタックス": [8, 6],
    "ジモン": [0, 3],
    "シグレニ": [12, 13],
    "アロメ": [2, 9],
    "ヤコ": [5, 7],
    "アナアキ": [1, 6],
    "エンペリー": [10, 11],
    "タタキケンサキ": [0, 10],
    "バラズシ": [13, 10],
    "シチリン": [13, 5],
    "クマサン商会": None,
    "アタリメイド": None,
    "amiibo": None
}

brand_weight = [2,2,2,2,2,2,2,2,2,2,2,2,2,2]
brand = brand_order[args.brand]
drink = None if args.drink == 'なし' else ability_order.index(args.drink)

def xor32(x32):
    x32 = x32 ^ (x32 << 13 & 0xFFFFFFFF)
    x32 = x32 ^ (x32 >> 17 & 0xFFFFFFFF)
    x32 = x32 ^ (x32 << 5 & 0xFFFFFFFF)
    return x32 & 0xFFFFFFFF

def max_brand_num():
    global brand_weight
    if brand is not None:
        brand_weight[brand[0]] = 10
        brand_weight[brand[1]] = 1
    return sum(brand_weight)

def max_brand_num_drink():
    global brand_weight
    if drink is not None:
        brand_weight[drink] = 0
    return sum(brand_weight)

def weighted_ability(ability_roll):
    global brand_weight
    ability = -1
    while(ability_roll >= 0):
        ability += 1
        ability_roll -= brand_weight[ability]
    return ability

def get_branded_ability():
    global seed
    ability_roll = seed % max_brand_num()
    return weighted_ability(ability_roll)

def get_branded_ability_drink():
    global seed
    ability_roll = seed % max_brand_num_drink()
    return weighted_ability(ability_roll)

def advance_seed():
    global seed
    seed = xor32(seed)

def get_ability():
    global seed
    advance_seed()
    ret = get_branded_ability()
    if drink is not None:
        if(seed % 0x64 <= 0x1D):
            return 0xFFFFFFFF
        advance_seed()
        ret = get_branded_ability_drink()
    return ret

def print_result():
    x = get_ability()
    if x == 0xFFFFFFFF:
        print(ability_order[drink])
    else:
        print(ability_order[x])

for i in range(args.display):
    print_result()

それでは、このコードの中から抜粋して解説していきます。

3つのリスト

抽選方式を解説するには、ツール(及びゲーム内)でのギアパワーの並び順(ability_order)、およびそれぞれのweight(brand_weight)という抽選率に関わる値を知る必要があります。
まず、ギアパワーは以下のような並びになっています。インク効率アップ(メイン)が0番で、アクション強化が13番です。

ability_order = [
    "インク効率アップ(メイン)",
    "インク効率アップ(サブ)",
    "インク回復力アップ",
    "ヒト移動速度アップ",
    "イカダッシュ速度アップ",
    "スペシャル増加量アップ",
    "スペシャル減少量ダウン",
    "スペシャル性能アップ",
    "復活時間短縮",
    "スーパージャンプ時間短縮",
    "サブ性能アップ",
    "相手インク影響軽減",
    "サブ影響軽減",
    "アクション強化"
]

weightはブランドごとに違います。
つきにくいギアパワーは1,普通のギアパワーは2,つきやすいギアパワーは10です。(amiibo,クマサン商会,アタリメイドは全て2)

brand_weight = [
    2, #インク効率アップ(メイン)
    2, #インク効率アップ(サブ)
    2, #インク回復力アップ
    2, #ヒト移動速度アップ
    2, #イカダッシュ速度アップ
    2, #スペシャル増加量アップ
    2, #スペシャル減少量ダウン
    2, #スペシャル性能アップ
    2, #復活時間短縮
    2, #スーパージャンプ時間短縮
    2, #サブ性能アップ
    2, #相手インク影響軽減
    2, #サブ影響軽減
    2  #アクション強化
]

#ブランドごとのつきやすい/つきにくいギアパワーのリスト
#0がインク効率アップ(メイン),13がアクション強化
brand_order = {
    "バトロイカ": [11, 0],
    "アイロニック": [9, 8],
    "クラーゲス": [4, 12],
    "ロッケンベルグ": [3, 4],
    "エゾッコ": [6, 5],
    "フォーリマ": [7, 1],
    "ホッコリー": [1, 2],
    "ホタックス": [8, 6],
    "ジモン": [0, 3],
    "シグレニ": [12, 13],
    "アロメ": [2, 9],
    "ヤコ": [5, 7],
    "アナアキ": [1, 6],
    "エンペリー": [10, 11],
    "タタキケンサキ": [0, 10],
    "バラズシ": [13, 10],
    "シチリン": [13, 5],
    "クマサン商会": None,
    "アタリメイド": None,
    "amiibo": None
}

get_ability

def get_ability():
    global seed
    advance_seed()
    ret = get_branded_ability()
    if drink is not None:
        if(seed % 0x64 <= 0x1D):
            return 0xFFFFFFFF
        advance_seed()
        ret = get_branded_ability_drink()
    return ret

このプログラムの要の部分ですので、1ブロックずつ解説していきましょう。

    advance_seed()
    ret = get_branded_ability()

まずはドリンクが関わらない部分です。
seedを1回進め、retにget_branded_abilityで抽選されたギアパワーのインデックスを格納しreturn retで返します。

ドリンクの抽選

if drink is not None:
        if(seed % 0x64 <= 0x1D):
            return 0xFFFFFFFF
        advance_seed()
        ret = get_branded_ability_drink()

ドリンクを使用する場合、30%の確率でドリンクで指定したギアパワーを抽選するため、seed値を100で割った余りが29以下の場合は強制的にドリンクで指定したギアパワーを返します。(return 0xFFFFFFFFの部分)
もしそれに該当しなければ、ドリンクで指定したギアパワーを除いたbrand_weightでget_branded_ability_drinkを実行し結果を返します。

advance_seed

(なぜこれでランダムっぽくなるのか、原理などはもっと詳しい記事がネットの海にありますのでxorshift32で検索してみてください)
名前の通り、XOR(排他的論理和)とSHIFT(ビットシフト)を使った疑似乱数列生成法の1つです。

def xor32(x32):
    x32 = x32 ^ (x32 << 13 & 0xFFFFFFFF)
    x32 = x32 ^ (x32 >> 17 & 0xFFFFFFFF)
    x32 = x32 ^ (x32 << 5 & 0xFFFFFFFF)
    return x32 & 0xFFFFFFFF

def advance_seed():
    global seed
    seed = xor32(seed)

下の関数(advance_seed)から上の関数(xor32)を呼び、seed値に新たな値を格納しています。

get_branded_abilityとmax_brand_num

def max_brand_num():
    global brand_weight
    if brand is not None:
        brand_weight[brand[0]] = 10
        brand_weight[brand[1]] = 1
    return sum(brand_weight)

def max_brand_num_drink():
    global brand_weight
    if drink is not None:
        brand_weight[drink] = 0
    return sum(brand_weight)

def get_branded_ability():
    global seed
    ability_roll = seed % max_brand_num()
    return weighted_ability(ability_roll)

def get_branded_ability_drink():
    global seed
    ability_roll = seed % max_brand_num_drink()
    return weighted_ability(ability_roll)

seed値から実際に何のギアパワーが抽選されるかの処理で、seedをmax_brand_numで割った余りをability_rollとしてweighted_abilityに渡し、抽選された0~13の数字(ability_orderのインデックス)を返します。
max_brand_numはweightの合計値です。(amiibo,クマサン商会,アタリメイドが28,それ以外が35)
get_branded_ability_drink,max_brand_num_drinkは、先程のドリンクによる30%抽選が外れた際の処理なので、ドリンクで指定したギアパワーを除外したweightの合計でability_rollを計算し、weighted_abilityに渡します。

weighted_ability

def weighted_ability(ability_roll):
    global brand_weight
    ability = -1
    while(ability_roll >= 0):
        ability += 1
        ability_roll -= brand_weight[ability]
    return ability

渡されたability_rollが0以下になるまで(ability_orderの順番で)weightで引き、0以下になった際のギアパワーのインデックスを返します。
weightの値が大きければ大きいほど、よりそのギアパワーが抽選されやすくなるのはこの処理が関係しているんですね。

print_result

def print_result():
    x = get_ability()
    if x == 0xFFFFFFFF:
        print(ability_order[drink])
    else:
        print(ability_order[x])

for i in range(args.display):
    print_result()

get_abilityを呼び、帰ってきた値が0xFFFFFFFFの場合はドリンクのギアパワーを、そうでない場合はability_orderの中から抽選されたギアパワーを表示します。
あとはこのprint_resultを任意の回数実行するだけで、Leanのツールと同じ結果が出力されるわけですね。

フローチャート

…さて、これでひととおりプログラムの説明は終わりなのですが、あまりにも可視化されておらずわかりづらいですよね…
というわけで、軽くフローチャートを作ってみました。
(ついでにコード全文も置いてあります。)

簡易バージョン


難しめバージョン

実践

python3の環境を持っている人であれば、コマンドプロンプトなりpowershellなり端末エミュレータなりで実行できます。

ちゃんとツールと結果が一致してますね!

まとめ

い か が で し た で し ょ う か ?

ここまで読んでくれている人がどれだけいるのか。なかなかプログラムの心得がある人でないと理解できないかもな記事になってしまいました。
ともあれ、私が書きたかった事はおおよそ書いたと思います。
Leangear seed checkerはアップデートが進み、ギアは3~4回の観測で特定可能(イカリング3と連携すれば新品のギアはワンボタンでseed特定可能)、ガチャの予測、サモランの報酬、スパイキーの注文の予測までできるようになっています。
こういったツールに忌避感が無い人は是非使ってみてください。

明日の記事はぼんじりさんによる「怪しいマイコンの話」です。
ポケモンSVが発売されてからswitch自動化もかなり注目を集めているので、atmegaに代わる安価なハードウェアが求められていますね。
私は(自称)NX信者なのでたいへん楽しみです。

それでは、また来年お会いしましょう。

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