Skip to content

Instantly share code, notes, and snippets.

@VienosNotes
Last active July 26, 2026 15:12
Show Gist options
  • Select an option

  • Save VienosNotes/18a35d84bc99561c6c99d36bf0f24779 to your computer and use it in GitHub Desktop.

Select an option

Save VienosNotes/18a35d84bc99561c6c99d36bf0f24779 to your computer and use it in GitHub Desktop.
ブルアカ新旧ガチャ比較
(*
手元で実行した結果
=== 旧仕様 ===
試行回数: 1,000,000 回
キャラA: 平均 111.174連 / 中央値 100.000連 / 標準偏差 69.558連
キャラB: 平均 196.951連 / 中央値 200.000連 / 標準偏差 86.174連
100連時点の1人目取得率: 50.456%
100連時点の両方取得率: 14.427%
200連時点の1人目取得率: 100.000%
200連時点の両方取得率: 75.540%
所要時間: 00:00:01.8521765
=== 新仕様 ===
試行回数: 1,000,000 回
キャラA: 平均 92.941連 / 中央値 100.000連 / 標準偏差 57.442連
キャラB: 平均 185.910連 / 中央値 180.000連 / 標準偏差 81.308連
100連時点の1人目取得率: 75.034%
100連時点の両方取得率: 14.461%
200連時点の1人目取得率: 100.000%
200連時点の両方取得率: 63.518%
所要時間: 00:00:01.6409699
*)
(*
Codex への指示
# 共通仕様
- ガチャは1連(単発)と10連があるが、このシミュレーションでは10連のみ考慮する
- かならずガチャは10連単位で実行する
- 現実的に単発で引くことはほぼないため無視する
- 排出されるキャラのレアリティは下から順に R1, R2, R3 の合計3段階ある
- ガチャにはピックアップ(以下PU)対象となるキャラがかならずあり、ある特定のキャラのみ確率が特別扱いされている
- PU 対象は必ず R3 のレアリティである
- 同一期間に複数種類の PU ガチャが開催されることがある
- これらはテーブルが区別されており、回す前にどの PU ガチャを回すかを選択する
- このシミュレーションの目的は、PU 対象のキャラを必ず入手すると決断したときにかかるコストを旧仕様・新仕様でそれぞれ求めるのを目的とする
- 特に2つの PU ガチャが開催されている場合に、両方のキャラを得るまで回すパターンをそれぞれシミュレーションする
- 10連ガチャの仕様は以下の通り
- 1~9枚目は以下の通りの確率である
- PU 対象 R3: 0.7%
- PU 対象外 R3: 2.3%
- R2: 18.5%
- R1: 78.5%
- 10枚目は以下の通りの確率である
- PU 対象 R3: 0.7%
- PU 対象外 R3: 2.3%
- R2: 97.0%
- 本来2キャラPUが開催されているときにキャラAのPUガチャでキャラBもR3枠で排出されうるが、確率は低いのでいったん無視する(排出されない)ことにする
# 旧仕様
- ガチャには天井がある。PU選択によらず共通で「呼び出しポイント」が10連ごとに10ずつ蓄積していく
- 「呼び出しポイント」は200ポイント、つまり200連ぶんのガチャを回すとPUのうち任意のキャラと引き換えることができる。これはガチャ外で任意のタイミングで引き換えを行うことができる
- たとえば、キャラAのPUを180連回してキャラAを得たとする。このときに「呼び出しポイント」は180溜まっているので、次にキャラBのPUを20連回せば合計200ポイントとなり、キャラBが仮に排出されなかったとしてもポイントでの引き換えでキャラBを得ることができる
- 「呼び出しポイント」は引き換えでのみ消費され、蓄積に上限はないが200ポイント単位でしか使用できない
- 「呼び出しポイント」はどのキャラの交換にも用いることができる
- 基本的な戦略は以下の通り
- まずAを対象に回す。Aを自然排出で取得したらBへ切り替える。ただし「呼び出しポイント」が200蓄積した時点でAが引き換え可能になるため、そこで停止してBに切り替える
- Aが排出された時点で「呼び出しポイント」が200以上蓄積していれば、ただちにBを引き換えて終了とする
- Aが排出された時点で「呼び出しポイント」が200未満の場合は、Bを対象に回し、自然排出するか200ポイント蓄積したら(引き換え可能になるので)終了とする
- Aが排出されないまま「呼び出しポイント」が200に達したら、ただちにAを引き換えて、以降はBに切り替え、自然排出するか200に達したらただちに引き換えて終了とする
- 交換の判定は、その10連の10枚すべての抽選結果を反映した後に行う。
- メモ:実際にはAのPUでもBがごく低確率ながら排出されうるため、現実の引き換え手順では裏目にならないよう引き換えを最後まで遅らせることになるが、本シミュレーションでは1人目のみ獲得したい場合の統計に影響が出るため上記の戦略とする
# 新仕様
- 呼び出しポイント制や天井は廃止
- PUによらず共通で「呼び出しチャージ」が10連ごとに10ずつ蓄積していく
- 「呼び出しチャージ」の累計が100になる10連を回した際にだけ、共通仕様のうち10枚目だけが以下のテーブルに変化する
- PU 対象 R3: 50%
- PU 対象外 R3: 50%
- 「呼び出しチャージ」の累計が200になる10連を回した際にだけ、共通仕様のうち10枚目だけが以下のテーブルに変化する
- PU 対象 R3: 100%
- 「呼び出しチャージ」の累計が100/200以外になる場合は共通仕様のテーブルで抽選を行う
- いかなるテーブルであれ、PU 対象キャラが排出された場合、「呼び出しチャージ」は0になる
- 10連の途中でPU対象が排出された場合、その次の10連の「呼び出しチャージ」が0から再開する
- 「呼び出しチャージ」が 100/200 になる10連のうち10枚目以外でPU対象が排出された場合も10枚目は 100/200 特有のテーブルとして抽選を行い、その次の10連は「呼び出しチャージ」が0から再開する
- 100 のテーブルでPUを外した場合は「呼び出しチャージ」はリセットされず、そのまま次の10連が110から再開する
- 基本的な戦略は以下の通り
- まずAを対象に回す。Aが排出されたら(この時点で「呼び出しチャージ」は必ず0になっている)次にBを対象に回す。Bが排出されたら終了
# 集計
任意の回数だけ、旧仕様・新仕様それぞれのルールでシミュレーションを行う。回数はソースにベタ書きでよい。
各シミュレーションの試行ごとに以下の2項目を記録しておく。集計は別途行うため、メモリ上に集計しやすい形で保持しておく。
- キャラAを入手したときの連数(100連目なら100)
- キャラBを入手したときの連数(Aが100連で出たあとBが50連で出たなら150)
記録が完了したら、新旧それぞれの上記2項目それぞれについて平均と中央値、標準偏差を出力する。
また、100連時点・200連時点で1人めのキャラを取得できた確率と、両方のキャラを取得できた確率をそれぞれパーセントで出力してください。
シミュレーションの回数と要した時間も出力する。
*)
open System
open System.Diagnostics
[<Struct>]
type TrialResult =
{ CharacterA: int
CharacterB: int }
[<Struct>]
type Statistics =
{ Mean: float
Median: float
StandardDeviation: float }
let simulationCount = 1_000_000
let pickupProbability = 0.007
let pullsPerBatch = 10
let oldExchangeCost = 200
let drawStandardTenPull (random: Random) =
let mutable pickupDrawn = false
let mutable index = 0
// PUの排出率は10枚目も0.7%なので、PUを引いたかだけを抽選すればよい。
while not pickupDrawn && index < pullsPerBatch do
pickupDrawn <- random.NextDouble() < pickupProbability
index <- index + 1
pickupDrawn
let drawNewTenPull (random: Random) charge =
let mutable pickupDrawn = false
let mutable index = 0
// 1~9枚目は、チャージ量にかかわらず通常のPU確率で抽選する。
while not pickupDrawn && index < pullsPerBatch - 1 do
pickupDrawn <- random.NextDouble() < pickupProbability
index <- index + 1
// 途中でPUが出ても、この10連の10枚目には開始時に決まった特殊テーブルを適用する。
let tenthPickupProbability =
match charge with
| 100 -> 0.5
| 200 -> 1.0
| _ -> pickupProbability
let tenthPickupDrawn = random.NextDouble() < tenthPickupProbability
pickupDrawn || tenthPickupDrawn
let simulateOldTrial (random: Random) =
let mutable totalPulls = 0
let mutable points = 0
let mutable characterA = 0
while characterA = 0 do
totalPulls <- totalPulls + pullsPerBatch
points <- points + pullsPerBatch
if drawStandardTenPull random then
characterA <- totalPulls
elif points >= oldExchangeCost then
points <- points - oldExchangeCost
characterA <- totalPulls
let mutable characterB = 0
// Aが200連目の10連で自然排出した場合は、残った200ポイントで即座にBを交換する。
if points >= oldExchangeCost then
points <- points - oldExchangeCost
characterB <- totalPulls
while characterB = 0 do
totalPulls <- totalPulls + pullsPerBatch
points <- points + pullsPerBatch
if drawStandardTenPull random then
characterB <- totalPulls
elif points >= oldExchangeCost then
points <- points - oldExchangeCost
characterB <- totalPulls
{ CharacterA = characterA
CharacterB = characterB }
let simulateNewTrial (random: Random) =
let mutable totalPulls = 0
let mutable charge = 0
let mutable characterA = 0
while characterA = 0 do
totalPulls <- totalPulls + pullsPerBatch
charge <- charge + pullsPerBatch
if drawNewTenPull random charge then
characterA <- totalPulls
charge <- 0
let mutable characterB = 0
while characterB = 0 do
totalPulls <- totalPulls + pullsPerBatch
charge <- charge + pullsPerBatch
if drawNewTenPull random charge then
characterB <- totalPulls
charge <- 0
{ CharacterA = characterA
CharacterB = characterB }
let calculateStatistics (values: int array) =
let count = values.Length
let mean = values |> Array.averageBy float
let sorted = Array.copy values
Array.sortInPlace sorted
let median =
if count % 2 = 1 then
float sorted[count / 2]
else
(float sorted[count / 2 - 1] + float sorted[count / 2]) / 2.0
let variance =
values
|> Array.averageBy (fun value ->
let difference = float value - mean
difference * difference)
{ Mean = mean
Median = median
StandardDeviation = sqrt variance }
let runSimulation name simulate =
let random = Random()
let stopwatch = Stopwatch.StartNew()
let results = Array.init simulationCount (fun _ -> simulate random)
stopwatch.Stop()
let characterAValues = results |> Array.map _.CharacterA
let characterBValues = results |> Array.map _.CharacterB
let characterAStatistics = calculateStatistics characterAValues
let characterBStatistics = calculateStatistics characterBValues
let acquiredAWithin100 =
characterAValues
|> Array.averageBy (fun pulls -> if pulls <= 100 then 1.0 else 0.0)
let acquiredBothWithin100 =
characterBValues
|> Array.averageBy (fun pulls -> if pulls <= 100 then 1.0 else 0.0)
let acquiredAWithin200 =
characterAValues
|> Array.averageBy (fun pulls -> if pulls <= 200 then 1.0 else 0.0)
let acquiredBothWithin200 =
characterBValues
|> Array.averageBy (fun pulls -> if pulls <= 200 then 1.0 else 0.0)
printfn "=== %s ===" name
printfn "試行回数: %s 回" (simulationCount.ToString("N0"))
printfn "キャラA: 平均 %.3f連 / 中央値 %.3f連 / 標準偏差 %.3f連" characterAStatistics.Mean characterAStatistics.Median characterAStatistics.StandardDeviation
printfn "キャラB: 平均 %.3f連 / 中央値 %.3f連 / 標準偏差 %.3f連" characterBStatistics.Mean characterBStatistics.Median characterBStatistics.StandardDeviation
printfn "100連時点の1人目取得率: %.3f%%" (acquiredAWithin100 * 100.0)
printfn "100連時点の両方取得率: %.3f%%" (acquiredBothWithin100 * 100.0)
printfn "200連時点の1人目取得率: %.3f%%" (acquiredAWithin200 * 100.0)
printfn "200連時点の両方取得率: %.3f%%" (acquiredBothWithin200 * 100.0)
printfn "所要時間: %s" (stopwatch.Elapsed.ToString())
printfn ""
runSimulation "旧仕様" simulateOldTrial
runSimulation "新仕様" simulateNewTrial
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment