複数のドロップアイテムの中からランダムにひとつ選ぶのは、randomモジュールを使えば簡単だ。だがそれら複数のドロップアイテムにはレアリティ差を作りたいのである。いくつか草案を書いてみる。

ひとつめ、まずこういうアイテムディクショナリを作っておく。"アイテムの名前":"ドロップしやすさ"という形式。

items = {
    "Fox"       : 5,  # よく出る
    "Reactive"  : 3,
    "Whitehorse": 1,  # 一番レア
    }

上のディクショナリを使ってリストを作る。そのリストには、アイテム名が"出やすさ"の数だけずらっと並ぶ。

lis = []
for item in items:
    lis.extend(item.split(" ") * items[item])

# こういうのができる
# lis = ['Whitehorse', 'Reactive', 'Reactive', 'Reactive',
#     'Fox', 'Fox', 'Fox', 'Fox', 'Fox']

あとは作ったリストからランダムにひとつ取り出すだけである。

import random
print(random.choice(lis))

# 5/9でFox、1/9でWhitehorseが出る

この手口の気になるところはlisの長さだろう。ゲーム中なんども発生するであろう作業のたびに、クソ長いリストを生成するのはどうにもエコノミックじゃない気がする。

というわけで別の書き方をしてみる。はじめのディクショナリこそ同じだけれど、その後に作るのはディクショナリをもとにしたアイテムとドロップしやすさをそれぞれリストにしたもの。

items = {
    "Fox"       : 5,   # よく出る
    "Reactive"  : 3,
    "Whitehorse": 1,   # 一番レア
    }
itemList = list(items.keys())
popList  = [items[item] for item in itemList]

# itemList : ['Whitehorse', 'Reactive', 'Fox']
# popList  : [1, 3, 5]

出やすさリストの合計を上限とした乱数をひとつ出し、その値以上の数になるまでpopListの要素を積み上げていく。

# レアリティの数値を全部積み上げたもの 今回なら9
maximum = sum(popList)
# 9を上限とした乱数を出し、アイテム4つからなる層(popList)のどこに位置するか調査する
target = random.randint(1, maximum)
i = 0
for index in range(len(popList)):
    i += popList[index]
    if target <= i:
        print(list(itemList)[index])

リストを作らないといけないのは同じだけれど、内部的にとるメモリ量はだいぶ少なくなったろう。多分。しらんけど。ためしにこれを9000回行い、アイテムが何度ずつドロップしたか集計してみた結果が以下。

{'Whitehorse': 1027, 'Reactive': 2941, 'Fox': 5032}

ビューリホー…。今回の最適解としてはコレかなあと思う。

ところでTwitterで、今回のような話題にちょっと応用できそうなツイートを見かけた。「x=1~10 x=1~x みたいに二重にした乱数は、数字が大きいほど出にくくなるので良さげ」とのこと。これはつまり何面サイコロを振るかをサイコロを振って決めるということだろう。今回のテーマに応用してみようと思ったが、そもそも、結果がどういう分布になるかが見当つかんのでそれを計算する関数を書いた。

def foo():
    # STARTとENDは自由に変えていいぜ
    START = 1
    END   = 10

    resultsList = []
    x = range(START, END + 1)

    for i in x:
        result1 = 1 / len(x)          # 最初のサイコロでiが出る確率
        result2 = 0
        for j in range(i, END + 1):   # 最初のサイコロで出た値から1までをまわす
            result2 += (1 / j)
        result = result1 * result2
        resultsList.append(result)
        print("結果が%sになる確率は%sです" % (i, round(result, 3)))

    print("(確認のため)合計は", sum(resultsList))

これを実行してみるとこうなる。

結果が1になる確率は0.293です
結果が2になる確率は0.193です
結果が3になる確率は0.143です
結果が4になる確率は0.11です
結果が5になる確率は0.085です
結果が6になる確率は0.065です
結果が7になる確率は0.048です
結果が8になる確率は0.034です
結果が9になる確率は0.021です
結果が10になる確率は0.01です
(確認のため)合計は 1.0

グラフ上で自然な曲線をかけそうで便利ではあるんだけど、俺のように、アイテムごとに出やすさを自由に設定したい場合はちと向かないかな?

今回はレアリティを3段階に設定して考えていたけども、条件によってはレア度の高いものほど先にコンプできてしまうということはありうる。たとえばトレーディングカードで1パックにつき「ノーマル3枚、レア1枚」が入っているとする。もしレアカードは3種類で、ノーマルカードが30種類だった場合、目当てのカードを入手できる確率はレアが1/3、ノーマルが1/10になる。そういう条件で考えると、レアリティの数値と実際のレア度が逆転することになってちょっと面白い。