Skip to content

Instantly share code, notes, and snippets.

@AltimorTASDK
Created August 24, 2022 15:10
Show Gist options
  • Save AltimorTASDK/40682cdfaa00c0d538cd2c061910f383 to your computer and use it in GitHub Desktop.
Save AltimorTASDK/40682cdfaa00c0d538cd2c061910f383 to your computer and use it in GitHub Desktop.
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