![見出し画像](https://assets.st-note.com/production/uploads/images/122242434/rectangle_large_type_2_3c8ea2f9f878bb28dca119669f790d36.png?width=1200)
【ユニオンアリーナ】012エナ必要枚数計算機
はじめに の前に。
2回に一回無事故で予選突破できりゃ良かろうという方は必要エナジー2以下を以下の割合で入れておけばおよそ問題ないかと。
必要エナジー0:11枚
必要エナジー1:4枚
必要エナジー2:10枚
はじめに
ユニオンアリーナでは最序盤に3エナジー発生させていれば大体やりたいことができます。ap回復の必要エナジーも3のため、上ブレも巻き返しもまずは3エナジー発生させるところからです。
3エナジー発生までは自分のデッキで完結するため、その確率を計算してみました。
先攻と後攻で条件が違いすぎるのと、先攻で事故らんかったらいけるやろの精神で先攻しか計算してません。(力尽きたとも言う)
0,12を合計20枚以下にしつつ、それなりに事故らない枚数の一覧です。
データの見方とその他の枚数の場合は最下部においています。
ここからは計算の中身の話です。
定義
手札事故の意味だったり、各種タイトルに、配られている共通した効果などについて略称を定義する。
いろいろ呼び方があるようなので違っていたらごめんなさい
事故
先攻1ターン目または2ターン目にエクストラドローをすること。
ここでのドローはかなり負けに近付きます。ライフにお祈りしましょう。
2個玉
読んで時のごとく、2個の玉(発生エナジー)が印刷されているカードのこと。デッキ構築の場面では一度条件を満たせば常時2エナジーを発生させるカードを含みます。アクティブ時に発生エナジーが2になるカードはその条件を取って、アクティブ2個玉などと呼ばれています。
バウンス2個玉
V.V.のこと。
必要エナジー1,発生2の登場時に必要エナジー1以下である自身の場のカードを手札に戻す(バウンス)する必要のあるカード。
炸裂2個玉
ユーフェミアのこと。
必要エナジー0,発生1のカード。起動メインでレストにするとターン中発生エナジーが1増える代わりに、アタックフェイズ移行時に退場する。
レスト2個玉などと呼ばれるのともあるが、退場要素を含まないのが気に食わないので私はこう呼ぶ。
トップチェック
あるいはトップコントロール。
ディートハルトのこと。
山札の上から何枚か見て、山札の下や場外あるいは山札の上に送る効果をもつカード。この記事においては1枚の前提。
ルーター
赤のC.C.のこと。
1ドロー1ディスする札。
ルーター"系"といったときはトップチェックもふくむ。
ソースコード概要
ユニオンアリーナのマリガンでは、初期手札を山札に戻さないで引き直す。
デッキの内訳が変わった状態のドローを計算ため、デッキオブジェクトを渡せば事故率が帰ってくる関数を考える。そうすれば手札チェック後、再度自身を呼び出せばマリガン時の確率まで計算できる。
(メソッドを再起的に使った際の挙動が分からなかった)
計算方法としては、事故が起きている手札の状況を想定し場合分けしながらそれぞれの確率を求める。
事故が起きている状況としては、エクストラドローが必要となった状況での発生エナジー数で分ければ良さそう。
最終的には、
ある事故手札を持っている確率*マリガン後も事故る確率
を合算して求める。
クラス定義
from math import comb
from dataclasses import dataclass
@dataclass
class Card():
num:int #マリガン中変動する数値
zero1:int #マリガン中変動する数値
zero2:int #マリガン中変動する数値
one1:int #マリガン中変動する数値
one2:int #マリガン中変動する数値
two:int #マリガン中変動する数値
top_check0:int =0 #マリガン中も変動しない数値
looter0:int =0 #マリガン中も変動しない数値
looter1:int =0 #マリガン中も変動しない数値
def __post_init__(self):
self.zero:int = self.zero1 + self.zero2
self.one:int = self.one1 + self.one2
self.other:int = self.num - self.zero - self.one - self.two
必要エナジー(英語)+発生エナジー(数字)
関数の定義
def brick_probability(
deck:Cards,
hand:int,
recursion = False , #マリガン含めてするか否か
hands:Cards = None
):
deck.__post_init__()
U = comb(deck.num,hand)
if deck.other < 0 or deck.zero < 0:
print("Deck error!")
return 0
#ここから先に追記していく。
0エナ事故
必要エナジー0のカードを引けていない状況。計算はここが一番簡単。
0以外のカードを7枚持つ組み合わせを計算すれば、あとは全体の組み合わせで割ってやれば良い。
マリガン前に,必要エナジー1,2のカードをどれだけ引いていたかでその後の事故率がかわるためループは一番深い。
また、マリガン後に再度この状況になってしまったら1ターン目エクストラドローが確定するので、トップ解決の確率などは計算しない。
#0エナ事故(発生エナジーが0のまま何もできない)
P_brick_zero = 0
for i in range(min(hand,deck.one1)+1):
for j in range(min(hand,deck.one2)+1):
for k in range(min(hand,deck.two)+1):
if hand < i+j+k:
continue
cb_one1 = max(1,comb(deck.one1,i))
cb_one2 = max(1,comb(deck.one2,j))
cb_two = max(1,comb(deck.two,k))
cb_other = max(1,comb(deck.other,hand-i-j-k))
cb_brick_zero = cb_one1 * cb_one2 * cb_two * cb_other
P_brick_zero_ijk = cb_brick_zero / U
if recursion:
drowed_deck = deepcopy(deck)
drowed_deck.num -= hand
drowed_deck.one1 -= i
drowed_deck.one2 -= j
drowed_deck.two -= k
discard_hand = Cards(hand,0,0,i,j,k)
P_rebrick_zero_ijk = \
brick_probability(drowed_deck,hand,False,discard_hand)
P_brick_zero_ijk *= P_rebrick_zero_ijk
else:
#先攻0エナ事故はエクドロ必須のため、トップチェックの計算をしない。
pass
P_brick_zero += P_brick_zero_ijk
手札事故はbrickedっていうらしい。
1エナ事故(1)
必要エナジー0のカードが1枚しかなく、必要エナジー1のカードを引けていない状況。炸裂2個玉を引いていた場合は別途考える。
組み合わせは、
デッキに入れた必要エナジー0の枚数(炸裂2個玉除く)
*デッキに入れた必要エナジー2以上のカード6枚の組み合わせ
マリガン後にこの状況に陥った場合、手札に必要エナジー2のカードがあるかないかで事故の確率が変わる。
ある場合は、デッキトップの必要エナジーが1以下であれば良いが、ない場合は2枚ドローしないと回避できないためルーターを引いていない場合は事故確定。(あってもつらい)
事故発生においてトップチェックとルーターの差が出るのはここだけ。
#1エナ事故############################################################
#zero1が1,残りは2以上
P_brick_one_1 = 0
if deck.zero1 > 0: #zero1が0枚ならそもそもここにこない
for i in range(min(hand-1,deck.two)+1): #1つはzero1なのでhand-1が上限
cb_two = max(1,comb(deck.two,i))
cb_other= max(1,comb(deck.other,hand-i-1))
cb_brick_one1_i = deck.zero1 * cb_two * cb_other
P_brick_one1_i = cb_brick_one1_i / U
if recursion:
drowed_deck = deepcopy(deck)
drowed_deck.num -= hand
drowed_deck.zero1 -= 1
drowed_deck.two -= i
discard_hand = Cards(hand,1,0,0,0,i)
P_rebrick_one1_i = \
brick_probability(drowed_deck,hand,False,discard_hand)
P_brick_one1_i *= P_rebrick_one1_i
else:
#ここからは追加ドロー分。マリガン前の手札を足さねばならない。
if i > 0: #2を引いていれば01ドローで事故回避の可能性がある。
#zero1を1ターン目に置く以外ないので、手札経由とトップコントロールは分けなくて良い。
#引いていたzero1がルーター系の確率 (zero2のルーター系の存在は想定しない)
P_drow_looter =max(0, min(1,(deck.top_check0 + deck.looter0) / (deck.zero1+hands.zero1)))
#トップが01でない確率
P_top1_over2 = 1 - ((deck.zero-1 + deck.one + hands.zero + hands.one) / deck.num)
#ルーター系を引かず、topが01でない確率
P_no_looter = (1-P_drow_looter) * P_top1_over2
#トップ2枚が01でない確率を求める。
#トップ2枚の組み合わせ
cb_top2 = comb(deck.num,2)
#トップ2枚が全て2以上の組み合わせ
cb_top2_over2 = comb(deck.num - (deck.zero-1 + deck.one + hands.zero + hands.one),2)
#トップ2枚が01でない確率
P_top2_over2 = cb_top2_over2 / cb_top2
#ルーター系を引いたが、top2枚が01でない確率
P_have_looter = P_drow_looter * P_top2_over2
P_brick_one1_i *= (P_have_looter + P_no_looter)
else: #2を引いていない時、0,1の手札経由を引いた場合、そこから2以下を引いてこれたならつながる。
#トップチェックとルーター両方を採用している時,1ターン目のカードによって変わる。
#が、プールにないか色が違うのでスルー。
###先攻のときを考えると、事故回避のために”2枚”ドローする必要がある。
#そのためトップチェックでは事故回避できない。
#また、エクドロまでに使えるルーターは高々1枚のため、1エナルーターは無視する。
if deck.looter0 >=1:
#引いていたzero1が手札経由の確率 (zero2のルーター系の存在は想定しない)
P_drow_looter =max(0, min(1,deck.looter0 / (deck.zero1+hands.zero1)))
# ルーター系を引いた状態で、トップ2枚について
# A:両方とも1以下 B:1枚は2で1枚は1以下
#のとき、事故回避する。
# Aの組み合わせ
cb_top2_under2 = comb(deck.zero-1 + deck.one + hands.zero + hands.one, 2)
# Bの組み合わせ
cb_top2_2_and_under2 = (deck.two + hands.two) * \
(deck.zero-1 + deck.one + hands.zero + hands.one)
# A or Bの起きる確率
P_AorB = (cb_top2_under2 + cb_top2_2_and_under2) / comb(deck.num,2)
#ルーター系を引いていて、都合よく事故回避する確率
P_tsugou = P_drow_looter * P_AorB
P_brick_one1_i *= (1-P_tsugou)
P_brick_one_1 += P_brick_one1_i
1エナ事故(2)
炸裂2個玉が1枚だけあり、必要エナジー2以下のカードを引けていない状況。組み合わせは、
デッキに入れた炸裂2個玉の枚数
*デッキに入れた必要エナジー3以上のカード6枚の組み合わせ
マリガン後でも、トップの必要エナジーが3以上なら再度事故
#1エナ事故###########################################################
#zero2が1,残りは3以上
P_brick_one_2 = 0
if deck.zero2 > 0: #zero2が0枚ならそもそもここにこない
cb_other = max(1,comb(deck.other,hand-1))
cb_brick_one_2 = deck.zero2 * cb_other
P_brick_one_2 =cb_brick_one_2 / U
if recursion:
drowed_deck = deepcopy(deck)
drowed_deck.num -= hand
drowed_deck.zero2 -= 1
discard_hand = Cards(hand,0,1,0,0,0)
P_rebirick_one_2 = \
brick_probability(drowed_deck,hand,False,discard_hand)
P_brick_one_2 *= P_rebirick_one_2
else:
#2以下を引けばトップで事故回避できる。(厳密には残りの手札が4以上など、だめなこともあるが考慮しない)
#zero2を1ターン目に置く以外ないので、通常ドロー以外の計算をしない。(zero2のルーター系は存在しない)
#トップが0~2でない確率
P_top1_over3 = 1 - ((deck.zero-1 + deck.one + deck.two + \
hands.zero + hands.one + hands.two) / deck.num)
P_brick_one_2 *= P_top1_over3
2エナ事故(1)
炸裂2個玉以外の必要エナジー0を二枚引いており、残りの必要エナジーが3以上の場合。炸裂2個玉があれば1ターン目においておけば2ターン目には3エナジー発生させられるので事故として扱わない。(というかそのためのカードでは?)
組み合わせは、
デッキに入れた必要エナジー0のカードから2枚選ぶ組み合わせ
*デッキに入れた必要エナジー3以上のカード5枚の組み合わせ
マリガン後はトップに必要エナジー2以下があれば良い。
引いた必要エナジー2以下のカードの枚数が、マリガン計算が少し楽。
#2エナ事故############################################################
#zero1が2,残りは3以上
P_brick_two_1 = 0
if deck.zero1 > 1: #zero1が1以下ならそもそもここにこない
cb_other = max(1,comb(deck.other,hand-2))
cb_brick_two_1 = comb(deck.zero1,2) * cb_other
P_brick_two_1 = cb_brick_two_1 / U
if recursion:
drowed_deck = deepcopy(deck)
drowed_deck.num -= hand
drowed_deck.zero1 -= 2
discard_hand = Cards(hand,2,0,0,0,0)
P_rebirick_two_1 = \
brick_probability(drowed_deck,hand,False,discard_hand)
P_brick_two_1 *= P_rebirick_two_1
else:
#2以下を引けばトップで事故回避の可能性がある。
#手札経由ならば2枚,トップチェックなら1枚まで使えるのでここで差がでる。
#トップチェックとルーター両方を採用している時,1ターン目のカードによって変わる。
#が、プールにないか色が違うのでスルー。(ルーター系があることを優先して計算)
#先攻について考えると、2枚目をもっていたところで1枚目&ドローで動きがとまるなら
#エクドロを入れるので関係ない。
#つまり、トップコントロールとルーターで区別しない。
looters_0 = deck.looter0 + deck.top_check0
#マリガン時に引いたzero1の2枚が両方ともルーター系ではない確率
P_no_looter = comb(((deck.zero1+hands.zero1) - looters_0),2) / comb((deck.zero1 + hands.zero1),2)
#トップが3以上の確率
P_top1_over3 = 1 - ((deck.zero-2 + deck.one + deck.two + \
hands.zero + hands.one + hands.two) / deck.num)
P_brick_no_looter = P_no_looter * P_top1_over3
#トップが2枚とも3以上の確率
#トップ2枚の組み合わせ
cb_top2 = comb(deck.num,2)
#トップ2枚が全て3以上の組み合わせ
cb_top2_over3 = comb(
deck.num - (deck.zero-2 + deck.one + deck.two +\
hands.zero + hands.one + hands.two)
,2)
#トップ2枚が3以上の確率
P_top2_over3 = cb_top2_over3 / cb_top2
P_brick_have_looter = (1- P_no_looter) * P_top2_over3
P_brick_two_1 *= (P_brick_no_looter + P_brick_have_looter)
2エナ事故(2)
必要エナジー0を1枚、バウンス2個玉でない必要エナジー1を1枚引いており、残りが3以上のとき。計算は2エナ事故(1)とほぼ同じ。
組み合わせは、
デッキに入れた必要エナジー0のカードの枚数
*デッキに入れたバウンス2個玉でない必要エナジー1のカードの枚数
*デッキに入れた必要エナジー3以上のカード5枚の組み合わせ
マリガン後はトップに必要エナジー2以下があれば良い。
#zero1が1,one1が1残りは3以上
P_brick_two_2 = 0
if deck.zero1 > 0 and deck.one1 > 0: #zero1やone1が0ならそもそもここに来ない
cb_other = max(1,comb(deck.other,hand-2))
cb_brick_two_2 = deck.zero1 * deck.one1 * cb_other
P_brick_two_2 = cb_brick_two_2 / U
if recursion:
drowed_deck = deepcopy(deck)
drowed_deck.num -= hand
drowed_deck.zero1 -= 1
drowed_deck.one1 -= 1
P_rebirick_two_2 = \
brick_probability(drowed_deck,hand,False,discard_hand)
P_brick_two_2 *= P_rebirick_two_2
else:
#2以下を引けばトップで事故回避の可能性がある。
#0をはじめに置く以外ないため、手札経由とトップコントロールは分けなくて良い。
looters_0 = deck.top_check0 + deck.looter0
#引いていた0がルーター系の確率
P_have_looter0 = looters_0 / (deck.zero + hands.zero)
P_no_looter = P_have_looter0
#<<先攻のとき>>
#1がルーター系だろうと、2ターン目のドロー終了時点で事故回避してなければエクドロするので無視する
#トップが3以上の確率
P_top1_over3 = 1 - ((deck.zero-2 + deck.one + deck.two + \
hands.zero + hands.one + hands.two) / deck.num)
P_brick_no_looter = P_no_looter * P_top1_over3
#トップが2枚とも3以上の確率
#トップ2枚の組み合わせ
cb_top2 = comb(deck.num,2)
#トップ2枚が全て3以上の組み合わせ
cb_top2_over3 = comb(
deck.num - (deck.zero-2 + deck.one + deck.two +\
hands.zero + hands.one + hands.two)
,2)
#トップ2枚が3以上の確率
P_top2_over3 = cb_top2_over3 / cb_top2
#ルーター系0の確率* top1_over3
P_brick_no_looter = (1-P_have_looter0) * P_top1_over3
#ルーター系1の確率* top2_over3
P_brick_have_looter = P_have_looter0 * P_top2_over3
P_brick_two_2 *= (P_brick_no_looter + P_brick_have_looter)
合算
P_brick = P_brick_zero + P_brick_one_1 + P_brick_one_2 + P_brick_two_1 + P_brick_two_2
return P_brick
ここまできたら、後はループをかけながら色々な組み合わせで調べてみるとよいでしょう。
試算
次のように内訳を表記し、事故発生率を示します。
(0エナ,炸裂2個玉,1エナ,バウンス2個玉,2エナ)
冒頭に記載したパターン
(11,0,4,0,10) = 9.8%
7連戦事故回避率 48.3%
若干足りませんね。というのも、ルーター系はあれば4投されると考えたためです。
この場合、事故率 9.2% 7戦連続回避 50.9% となり大体おっけーです。
うろ覚えですがコードギアスのスタートデッキも調べてみる。
(12,4,4,2,4) = 1.0%
まさにスタートデッキ。事故とは縁遠いね。
最後に、「01を頑張って減らしてみました!」です。
炸裂もバウンス2個玉も入れたくない。けれど2エナ以下の枚数も減らしたい(3以上をいっぱい入れたい)といった時の構築でしょうか。
(11,0,3,6) = 17.3%
7戦連続事故回避率 26.5%
4回地域予選に行けば一回は決勝トーナメント進出できそうですね。
おとなしく0と1の枚数を増やしましょう。
おわりに
必要エナジー3以上の強力な札の枚数だけ決めてやればあとは簡単に0~2の枚数が決まるので非常に便利な関数になったと思います。
デッキの構成を入力するだけのWEBページなど作ればさらに使いやすいと思いますが、そのためにサーバを立てるのもなんだかなぁといった形。
アプリ開発にはまだまだ勉強が必要そうです。
お読みいただきありがとうございました。
(2023/11/20 ソースコードを記載。試算追加)
(202312/7 用語修正 おまけ部分を移動)
(2024/05/07 貼付ファイル移動)
事故回避率一覧
必要エナジー2以下のカードの合計枚数を20枚以下にした時の事故率一覧と、21~26枚で7戦連続事故回避率49%以上の組合を示すCSVです。
データは下記のように記載しています。
1列目:列番号
2列目:必要エナジー0,発生エナジー1の枚数
3列目:炸裂2個玉の枚数
4列目:必要エナジー1,発生エナジー1の枚数
5列目:バウンス2個玉の枚数
6列目:必要エナジー2の枚数
7列目:事故回避率
8列目:ルーターの枚数