Created
August 24, 2022 15:10
-
-
Save AltimorTASDK/40682cdfaa00c0d538cd2c061910f383 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
import numpy | |
from numpy import int32, float64 | |
import random | |
numpy.seterr(all='ignore') | |
SUMMONS = { | |
'421A': [ | |
[["Vtuber"],4], | |
[["FGO_S"],4], | |
[["FGO_C"],3], | |
[["Tmitter"],3], | |
[["Necocan"],2], | |
[["SOS"],1], | |
[["Light"],0], | |
[["Train"],0] | |
], | |
'421B': [ | |
[["EcoArc"],4], | |
[["DashNecos"],4], | |
[["FallInNeco","FallInNeco","FallInNeco"],4], | |
[["FallInNeco","UndergroundInNeco"],3], | |
[["FallInNeco","FallInNeco"],2], | |
[["FallInNeco","UndergroundInNeco"],2], | |
[["FallInNeco","FallInNeco"],1], | |
[["UndergroundInNeco","FallInNeco"],1], | |
[["TirePull","UndergroundInNeco"],1], | |
[["FallInNeco","UndergroundInNeco"],0], | |
[["UndergroundInNeco","FallInNeco"],0] | |
], | |
'421C': [ | |
[["SOS","Vtuber","Light"],4], | |
[["Light","FGO_S","Train"],4], | |
[["SOS","FGO_S","Light"],4], | |
[["Light","FGO_S","SOS"],4], | |
[["Necocan","FGO_C","Train"],3], | |
[["SOS","FGO_C","Light"],3], | |
[["Light","FGO_C","SOS"],3], | |
[["Light","Tmitter","Train"],2], | |
[["Light","Necocan","SOS"],0], | |
[["Train","SOS","Light"],0] | |
], | |
'1BC': [ | |
[["EcoArc","DashNecos"],4], | |
[["EcoArc","FallInNeco"],4], | |
[["EcoArc","UndergroundInNeco"],4], | |
[["FallInNeco","FallInNeco","UndergroundInNeco"],3], | |
[["FallInNeco","UndergroundInNeco","FallInNeco"],3], | |
[["DashNecos","FallInNeco","UndergroundInNeco"],2], | |
# Typo in the original: | |
# [["UndergroundInNeco","FallInNeco""FallInNeco"],2], | |
[["UndergroundInNeco","FallInNeco"],2], | |
[["DashNecos","FallInNeco","UndergroundInNeco"],2], | |
[["FallInNeco","UndergroundInNeco","FallInNeco"],1], | |
[["UndergroundInNeco","FallInNeco","DashNecos"],1], | |
[["UndergroundInNeco","FallInNeco","DashNecos"],0], | |
[["UndergroundInNeco","FallInNeco","TirePull"],0] | |
] | |
} | |
NECO_PAT_LIST = [ | |
"Neco0 - (Useless) Lay down", | |
"Neco1 - Dash with hitbox", | |
"Neco2 - 3C", | |
"Neco3 - [A+D]", | |
"Neco4 - Super punch", | |
"Neco5 - (Useless) Walking", | |
"Neco6 - (Useless) Crawling", | |
"Neco7 - j.214A", | |
"Neco8 - 22[A]", | |
"Neco9 - Rapid Beat 2", | |
"Neco10 - 623A", | |
"Neco11 - 236B", | |
"Neco12 - 5C", | |
"Neco13 - (Useless) Celebration", | |
"Neco14 - 5[B]", | |
"Neco15 - 5B", | |
"Neco16 - Mid-angled beam", | |
"Neco17 - 6A>6A>6A", | |
"Neco18 - (Useless) Turnaround" | |
] | |
# ランダムで色々な行動に飛ぶ | |
NECO_LIST = [ | |
# 行動+必要幸運値 | |
[NECO_PAT_LIST[8],4], # すごいアバドンフレア 大当たり枠 | |
[NECO_PAT_LIST[9],4], # オラオラ 当たり枠 | |
[NECO_PAT_LIST[0],0], # しゃがみ | |
[NECO_PAT_LIST[1],1], # ダッシュ | |
[NECO_PAT_LIST[2],1], # 3C | |
[NECO_PAT_LIST[3],1], # ぐるぐるパンチ | |
[NECO_PAT_LIST[4],3], # 鉄拳 | |
[NECO_PAT_LIST[5],0], # 歩き | |
[NECO_PAT_LIST[6],0], # しゃがみ歩き | |
[NECO_PAT_LIST[7],2], # ワニ園 | |
[NECO_PAT_LIST[10],2], # 反復横跳び | |
[NECO_PAT_LIST[11],3], # Bビーム | |
[NECO_PAT_LIST[12],1], # 立ち強 | |
[NECO_PAT_LIST[13],0], # 勝ちポーズ | |
[NECO_PAT_LIST[14],3], # タメ立ち中 | |
[NECO_PAT_LIST[15],1], # 立ち中 | |
[NECO_PAT_LIST[16],4], # 斜めビーム | |
[NECO_PAT_LIST[17],1], # ジャブ | |
[NECO_PAT_LIST[18],0] # 振り向き | |
] | |
class Random(): | |
def __init__(self, seed=None): | |
self.table = [int32(0)] * 56 | |
self.index = 0 | |
self.set_seed(seed) | |
def set_seed(self, seed=None): | |
if seed is None: | |
seed = random.randrange(1 << 31) | |
value = int32(0x9A4EC86) - int32(abs(seed)) | |
self.table[55] = value | |
next_value = int32(1) | |
for i in range(1, 55): | |
prev_value = value | |
value = next_value | |
self.table[i * 21 % 55] = value | |
next_value = self._to_positive(prev_value - value) | |
for _ in range(0, 4): | |
for i in range(1, 56): | |
value = self.table[i] - self.table[(i + 30) % 55 + 1] | |
self.table[i] = self._to_positive(value) | |
def next(self): | |
self.index = self.index % 55 + 1 | |
value = self.table[self.index] - self.table[(self.index + 20) % 55 + 1] | |
value = self._to_positive(value) | |
self.table[self.index] = value | |
return value | |
def randint(self, limit): | |
return int(float64(self.next()) * (1.0 / 2147483647.1) * limit) | |
def _to_positive(self, num): | |
return num + 0x7FFFFFFF if num < 0 else num | |
class Cat(): | |
_random = Random() | |
def __init__(self, *, luck=0, hp=10000, heat=False, moon=False): | |
self.start_luck = luck | |
self.luck = luck | |
self.hp = hp | |
self.heat = heat | |
self.moon = moon | |
self._vtuber = False | |
def reset(self): | |
self.luck = self.start_luck | |
self._vtuber = False | |
def is_finished(self): | |
# for RiggedCat | |
return False | |
def roll_summons(self, choices): | |
if self._vtuber and 'Vtuber' in choices[0][0]: | |
choices = choices[1:] | |
results = choices[self._gachaPick(choices)[0]] | |
return (tuple(results[0]), results[1]) | |
def process_summons(self, summons): | |
# Advance RNG and roll necos | |
for summon in summons[0]: | |
self._do_summon(summon) | |
def roll_neco(self): | |
index = self._gachaPick(NECO_LIST)[0] | |
return tuple(NECO_LIST[index]) | |
def result_weight(self): | |
return 1 | |
def _get_weights(self): | |
return [100, 50, 20] | |
def _Random_Limit(self, limit): | |
return self._random.randint(limit) | |
def _gachaPick(self, _moveList): | |
now_gachaLuck = self.luck # 現在の幸運値 | |
use_gachaLuck = now_gachaLuck // 2 # 計算に使用する幸運値(そのままだと増減の影響が大きいのでPPの半分の値を使う) | |
# 幸運値の固定増加 | |
# 体力低下or強制開放中orムーンドライブ中だとガチャ運アップ | |
if self.hp <= 3000: | |
use_gachaLuck += 2 # 体力3000以下 | |
elif self.hp <= 4500: | |
use_gachaLuck += 1 # 体力4500以下 | |
if self.heat: | |
use_gachaLuck += 2 # 強制開放中 | |
if self.moon: | |
use_gachaLuck += 1 # ムーンドライブ中 | |
# 幸運EX以上はリストの先頭を返す | |
if use_gachaLuck >= 5: | |
return 0 | |
# 幸運値が同じなら重み100として重み付き抽選。幸運値が一段階離れるごとに重み減少 | |
weightList = self._get_weights() # 幸運値の差で重みが変わる | |
weightsum = 0 # 抽選の重みの和 | |
while True: | |
for val in _moveList: | |
luckLv = val[1] | |
luckDiff = abs(use_gachaLuck - luckLv) # 現在の幸運値との差の絶対値 | |
if luckDiff < len(weightList): | |
weightsum += weightList[luckDiff] # 基準を100として差があれば重み軽減 | |
# 何も抽選されていないなら幸運値を1下げて再計算 | |
if weightsum == 0 and use_gachaLuck > 0: | |
use_gachaLuck -= 1 | |
elif weightsum > 0: | |
break | |
else: | |
return 0 | |
# 重みの和から乱数生成 | |
rand = self._Random_Limit(weightsum) # 乱数 | |
retIndex = 0 # 抽選結果 | |
for ii in range(len(_moveList)): | |
luckLv = _moveList[ii][1] # 必要幸運値 | |
luckDiff = abs(use_gachaLuck - luckLv) # 現在の幸運値との差の絶対値 | |
# luckDiffが3未満なら採用 | |
if luckDiff < 3: | |
# 乱数が重みの値未満なら当選(一般的な重み付き抽選処理) | |
omomi = weightList[luckDiff] | |
if omomi > rand: | |
retIndex = ii | |
# 幸運値を変動する | |
if luckLv == 4: | |
# 大当たり枠が出たら大きく減らす | |
self.luck -= 4 | |
elif now_gachaLuck >= luckLv*2: # 現在値と比較(now_gachaLuckは1/2するので比較側を2倍) | |
# 同値以下が当選 :+1(最大+6まで) | |
self.luck += 1 | |
else: | |
# 同値より上が当選 :-1(0以下にはならない) | |
self.luck -= 1 | |
if self.luck > 6: self.luck = 6 | |
if self.luck < 0: self.luck = 0 | |
break | |
rand -= omomi # 抽選対象外になったやつの重みを引く | |
return (retIndex, use_gachaLuck) | |
def _do_summon(self, summon): | |
if summon == 'Vtuber': | |
self._vtuber = True | |
elif summon in ['JumpInNeco', 'FallInNeco', 'UndergroundInNeco']: | |
self.roll_neco() | |
class RiggedCat(Cat): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self._last_summon = None | |
self._rolls = [] | |
self._roll_limits = [] | |
self._roll_index = 0 | |
self._weight_total = 1 | |
self._finished = False | |
def reset(self): | |
super().reset() | |
# Increment last roll and propagate up when it reaches its max | |
index = self._roll_index - 1 | |
self._rolls[index] += 1 | |
while self._rolls[index] == self._roll_limits[index]: | |
if index == 0: | |
# All rolls are maxed | |
self._finished = True | |
break | |
self._rolls[index] = 0 | |
self._roll_limits[index] = None | |
self._rolls[index - 1] += 1 | |
index -= 1 | |
self._roll_index = 0 | |
self._weight_total = 1 | |
def is_finished(self): | |
return self._finished | |
def result_weight(self): | |
# Use multiplied weight from all rolls | |
return self._weight_total | |
def _get_weights(self): | |
# Don't duplicate entries | |
return [1, 1, 1] | |
def _Random_Limit(self, limit): | |
index = self._roll_index | |
self._roll_index += 1 | |
if index == len(self._rolls): | |
# Add new roll | |
self._rolls.append(0) | |
self._roll_limits.append(limit) | |
return 0 | |
if self._roll_limits[index] is None: | |
self._roll_limits[index] = limit | |
elif limit != self._roll_limits[index]: | |
raise Exception("aww hell naw") | |
return self._rolls[index] | |
def _gachaPick(self, _moveList): | |
result = super()._gachaPick(_moveList) | |
weight_map = super()._get_weights() | |
# Calculate fraction of weight total for this roll | |
luck_diff = [abs(v[1] - result[1]) for v in _moveList] | |
weights = [weight_map[x] if x < 3 else 0 for x in luck_diff] | |
self._weight_total *= weights[result[0]] / sum(weights) | |
return result | |
class SampleSet(): | |
def __init__(self): | |
self._samples = [] | |
self._weights = [] | |
self._counts = {} | |
self._total_weight = 0 | |
def add(self, value, weight=1): | |
self._samples.append(value) | |
self._weights.append(weight) | |
self._counts[value] = self._counts.get(value, 0) + weight | |
self._total_weight += weight | |
def average(self): | |
return numpy.average(self._samples, weights=self._weights) | |
def counts(self): | |
return dict(sorted(self._counts.items(), key=lambda kv: -kv[1])) | |
def ratios(self): | |
return {k: v / self._total_weight for k, v in self.counts().items()} | |
class TestResults(): | |
def __init__(self): | |
self.net_luck = SampleSet() | |
self.summons = SampleSet() | |
def test_summons(choices, luck, iterations): | |
results = TestResults() | |
cat = Cat(luck=luck) if iterations >= 0 else RiggedCat(luck=luck) | |
while iterations != 0 and not cat.is_finished(): | |
summons = cat.roll_summons(choices) | |
summon_net_luck = cat.luck - luck # Before neco rolls | |
cat.process_summons(summons) | |
weight = cat.result_weight() | |
results.net_luck.add(cat.luck - luck, weight) | |
results.summons.add(summons + (summon_net_luck,), weight) | |
cat.reset() | |
iterations -= 1 | |
return results | |
def test_necos(neco_count, luck, iterations): | |
results = TestResults() | |
cat = Cat(luck=luck) if iterations >= 0 else RiggedCat(luck=luck) | |
while iterations != 0 and not cat.is_finished(): | |
summons = [cat.roll_neco() for _ in range(neco_count)] | |
weight = cat.result_weight() | |
for summon in summons: | |
results.summons.add(summon, weight) | |
results.net_luck.add(cat.luck - luck, weight) | |
cat.reset() | |
return results | |
def print_indent(level): | |
print(" " * level, end="") | |
def print_summons(command, luck, iterations): | |
results = test_summons(SUMMONS[command], luck, iterations) | |
print(f"Luck {luck} {command}") | |
print_indent(1) | |
print(f"Net luck (average {results.net_luck.average():+7.4f}):") | |
for net_luck, ratio in results.net_luck.ratios().items(): | |
print_indent(2) | |
print(f"{ratio*100:6.2f}%: {net_luck:+d}") | |
print_indent(1) | |
print("Summons:") | |
for summons, ratio in results.summons.ratios().items(): | |
names = " ".join(name.ljust(17) for name in summons[0]) | |
level = summons[1] | |
net_luck = summons[2] | |
print_indent(2) | |
print(f"{ratio*100:6.2f}%: Lv{level} Luck{net_luck:+d} - {names}") | |
def print_necos(neco_count, luck, iterations): | |
results = test_necos(neco_count, luck, iterations) | |
print(f"Luck {luck} Necos x{neco_count}") | |
print_indent(1) | |
print(f"Net luck (average {results.net_luck.average():+7.4f}):") | |
for net_luck, ratio in results.net_luck.ratios().items(): | |
print_indent(2) | |
print(f"{ratio*100:6.2f}%: {net_luck:+d}") | |
print_indent(1) | |
print("Necos:") | |
for neco, ratio in results.summons.ratios().items(): | |
print_indent(2) | |
print(f"{ratio*100:6.2f}%: Lv{neco[1]} - {neco[0]}") | |
def print_tests(luck, iterations): | |
print("-" * 80) | |
for command in SUMMONS: | |
print_summons(command, luck, iterations) | |
for necos in range(3): | |
print_necos(necos + 1, luck, iterations) | |
def main(): | |
for luck in range(0, 7): | |
print_tests(luck, -1) | |
print("-" * 80) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment