├── .gitignore ├── GGanalysis ├── ReverseEngineering │ ├── artifacts_like_autocracker.py │ └── gacha_model_autocracker.py ├── ScoredItem │ ├── __init__.py │ ├── genshin_like_scored_item.py │ ├── scored_item.py │ ├── scored_item_plot.py │ └── scored_item_tools.py ├── SimulationTools │ ├── scored_item_sim.py │ └── statistical_tools.py ├── __init__.py ├── basic_models.py ├── coupon_collection.py ├── distribution_1d.py ├── gacha_layers.py ├── gacha_plot.py ├── games │ ├── __init__.py │ ├── alchemy_stars │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ └── stationary_p.py │ ├── arknights │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ ├── readme.md │ │ └── stationary_p.py │ ├── arknights_endfield │ │ ├── __init__.py │ │ ├── gacha_model.py │ │ └── stationary_p.py │ ├── azur_lane │ │ ├── __init__.py │ │ └── gacha_model.py │ ├── blue_archive │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ └── gacha_model.py │ ├── genshin_impact │ │ ├── __init__.py │ │ ├── artifact_data.py │ │ ├── artifact_example.py │ │ ├── artifact_model.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ ├── get_both_EP_weapon.py │ │ ├── get_cost.py │ │ ├── predict_next_type.py │ │ ├── readme.md │ │ └── stationary_p.py │ ├── girls_frontline2_exilium │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ └── stationary_p.py │ ├── honkai_impact_3rd_v2 │ │ ├── __init__.py │ │ └── gacha_model.py │ ├── honkai_star_rail │ │ ├── __init__.py │ │ ├── gacha_model.py │ │ ├── relic_data.py │ │ ├── relic_model.py │ │ ├── relic_sim.py │ │ └── stationary_p.py │ ├── reverse_1999 │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ └── stationary_p.py │ ├── wuthering_waves │ │ ├── __init__.py │ │ ├── figure_plot.py │ │ ├── gacha_model.py │ │ └── stationary_p.py │ └── zenless_zone_zero │ │ ├── __init__.py │ │ ├── gacha_model.py │ │ └── stationary_p.py ├── markov_method.py └── plot_tools.py ├── LICENSE ├── docs ├── Makefile ├── make.bat ├── readme.md ├── requirements.txt └── source │ ├── _static │ └── custom.css │ ├── conf.py │ ├── games │ ├── alchemy_stars │ │ └── index.rst │ ├── arknights │ │ └── index.rst │ ├── azur_lane │ │ └── index.rst │ ├── blue_archive │ │ └── index.rst │ ├── genshin_impact │ │ ├── artifact_models.rst │ │ ├── gacha_models.rst │ │ └── index.rst │ ├── girls_frontline2_exilium │ │ └── index.rst │ ├── honkai_star_rail │ │ ├── gacha_models.rst │ │ ├── index.rst │ │ └── relic_models.rst │ ├── index.rst │ ├── reverse_1999 │ │ └── index.rst │ ├── wuthering_waves │ │ └── index.rst │ └── zenless_zone_zero │ │ └── index.rst │ ├── index.rst │ ├── introduction_to_gacha │ ├── feedback_item_problem.rst │ ├── foundations.rst │ ├── index.rst │ └── statistical_modeling_methods.rst │ ├── reference_manual │ ├── basic_tools.rst │ └── index.rst │ └── start_using │ ├── basic_concepts.rst │ ├── check_gacha_plan.rst │ ├── custom_gacha_model.rst │ ├── environment_installation.rst │ ├── index.rst │ ├── quick_visualization.rst │ ├── stationary_distribution.rst │ └── use_predefined_model.rst ├── readme.md └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.pyc 4 | 5 | # temp files 6 | *.o 7 | *.png 8 | *.svg 9 | test*.py 10 | temp.csv 11 | temp_plot.py 12 | temp_use.py 13 | GGanalysis.egg-info/ 14 | doctrees/ 15 | figure/ 16 | build/ 17 | dist/ 18 | 19 | # leave logo file 20 | !/docs/source/logo.svg 21 | 22 | # test files 23 | test.py 24 | temp.py 25 | .DS_Store 26 | launch.json 27 | GGanalysislib/ 28 | 29 | # vs code setting 30 | .vscode/ 31 | 32 | # idea setting 33 | .idea/ -------------------------------------------------------------------------------- /GGanalysis/ReverseEngineering/artifacts_like_autocracker.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 用于根据统计数据解析类似原神圣遗物模型的副词条权重 3 | ''' 4 | import itertools 5 | import math 6 | 7 | def calc_weighted_selection_logp(elements: list, perm: list, removed: list=None, sum_each_perm=True): 8 | '''**计算不放回抽样概率的对数** 9 | 10 | - ``elements`` : 不放回词条权重 11 | - ``perm`` :道具当前选中词条的排列 12 | - ``removed`` :被排除的词条(如原神圣遗物中的主词条) 13 | - ``sum_each_perm`` :是否将同组合下所有排列概率加和(即不考虑词条获取顺序的情况) 14 | ''' 15 | def calc_p(perm_labels): 16 | # 计算选择出指定排列的概率 17 | p = 1 18 | weight_all = sum(elements) 19 | if removed is not None: 20 | for i in removed: 21 | weight_all -= elements[i] 22 | for label in perm_labels: 23 | p *= elements[label] / weight_all 24 | weight_all -= elements[label] 25 | return p 26 | if not sum_each_perm: 27 | return math.log(calc_p(perm)) 28 | # 计算每种排列情况之和 29 | ans = 0 30 | perm = sorted(perm) 31 | permutations = itertools.permutations(perm) 32 | for perm in permutations: 33 | ans += calc_p(perm) 34 | return math.log(ans) 35 | 36 | def construct_likelihood_function(elements: list, selected_perms: list[list], perm_numbers: list, removes: list, sum_each_perm=True): 37 | '''**构建当前情况不放回抽样的似然函数** 38 | 39 | - ``elements`` : 不放回词条权重(优化量) 40 | - ``selected_perms`` :道具当前选中词条的排列 41 | - ``perm_numbers`` :每种排列出现的次数 42 | - ``removed`` :每种排列被排除的词条(如原神圣遗物中的主词条) 43 | - ``sum_each_perm`` :是否将同组合下所有排列概率加和(即不考虑词条获取顺序的情况) 44 | 45 | 似然函数为 :math:`\log{\pord_{all condition}{P_{condition}^{appear times}}}` ,即对样本中每种出现的主词条-副词条组合记录出现次数, 46 | 将每种情况出现的概率乘方其出现次数,将所有情况的值乘在一起取对数即为本次试验的似然函数。 47 | ''' 48 | return sum(number * calc_weighted_selection_logp(elements, perm, remove, sum_each_perm) for perm, number, remove in zip(selected_perms, perm_numbers, removes)) 49 | 50 | 51 | if __name__ == '__main__': 52 | from scipy.optimize import minimize 53 | import numpy as np 54 | pass -------------------------------------------------------------------------------- /GGanalysis/ReverseEngineering/gacha_model_autocracker.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 用于自动根据综合概率构建抽卡模型 3 | ''' 4 | import numpy as np 5 | from GGanalysis.distribution_1d import p2dist, calc_expectation 6 | 7 | class LinearAutoCracker(): 8 | '''**概率线性上升软保底模型自动解析工具** 9 | 10 | - ``base_p`` : 基础概率 11 | - ``avg_p`` :综合概率 12 | - ``hard_pity`` :硬保底抽数 13 | - ``pity_begin`` :概率开始上升位置(可选) 14 | - ``early_hard_pity`` :是否允许实际硬保底位置提前,默认为否 15 | - ``forced_hard_pity`` :是否允许概率上升曲线中硬保底位置概率小于1(此时硬保底位置会被直接设为1) 16 | 17 | ''' 18 | def __init__(self, base_p, avg_p, hard_pity, pity_begin=None, early_hard_pity=False, forced_hard_pity=False) -> None: 19 | self.base_p = base_p 20 | self.avg_p = avg_p 21 | self.hard_pity = hard_pity 22 | self.pity_begin = pity_begin 23 | self.early_hard_pity = early_hard_pity 24 | self.forced_hard_pity = forced_hard_pity 25 | pass 26 | 27 | def calc_avg_p(self, p_list): 28 | '''根据概率提升表计算综合概率''' 29 | return 1/calc_expectation(p2dist(p_list)) 30 | 31 | def search_params(self, step_value=0.001, min_pity_begin=1): 32 | '''搜索符合要求的概率上升表,返回比设定综合概率高或低的参数组合中最接近的 33 | 34 | - ``step_value`` : 概率上升值变动的最小值 35 | - ``min_pity_begin`` : 设定最早的概率开始上升位置 36 | ''' 37 | p_list = np.zeros(self.hard_pity+1) 38 | upper_pity_pos = -1 39 | upper_step_p = -1 40 | upper_p = -1 41 | lower_pity_pos = -1 42 | lower_step_p = -1 43 | lower_p = -1 44 | 45 | pity_begin_list = range(min_pity_begin, self.hard_pity+1) 46 | if self.pity_begin is not None: 47 | pity_begin_list = [self.pity_begin] 48 | for i in pity_begin_list: 49 | step_p = 0 50 | while(step_p < 1): 51 | step_p += step_value 52 | p_list[:i] = self.base_p 53 | p_list[i:] = np.arange(1, self.hard_pity-i+2) * step_p + self.base_p 54 | if p_list[self.hard_pity] < 1 and not self.forced_hard_pity: 55 | # 是否不满足最末尾上升到1 56 | continue 57 | if p_list[self.hard_pity-1] > 1 and not self.early_hard_pity: 58 | # 是否在硬保底位置前上升到1 59 | break 60 | p_list = np.minimum(1, p_list) 61 | p_list[-1] = 1 62 | estimated_p = self.calc_avg_p(p_list) 63 | if estimated_p >= self.avg_p: 64 | if estimated_p - self.avg_p < abs(upper_p - self.avg_p): 65 | upper_pity_pos = i 66 | upper_step_p = step_p 67 | upper_p = estimated_p 68 | else: 69 | if self.avg_p - estimated_p < abs(self.avg_p - lower_p): 70 | lower_pity_pos = i 71 | lower_step_p = step_p 72 | lower_p = estimated_p 73 | return (self.base_p, upper_pity_pos, upper_step_p, float(upper_p)), (self.base_p, lower_pity_pos, lower_step_p, float(lower_p)) 74 | 75 | if __name__ == '__main__': 76 | # genshin_cracker = LinearAutoCracker(0.006, 0.016, 90) 77 | # print(genshin_cracker.search_params(step_value=0.01)) 78 | # hsr_weapon_cracker = LinearAutoCracker(0.008, 0.0187, 80) 79 | # print(hsr_weapon_cracker.search_params(step_value=0.01)) 80 | # wuwa_cracker = LinearAutoCracker(0.008, 0.018, 80, pity_begin=66, forced_hard_pity=True) 81 | # print(wuwa_cracker.search_params(step_value=0.04)) 82 | zzz_cracker = LinearAutoCracker(0.01, 0.02, 80, pity_begin=65, forced_hard_pity=True) 83 | print(zzz_cracker.search_params(step_value=0.01)) 84 | -------------------------------------------------------------------------------- /GGanalysis/ScoredItem/__init__.py: -------------------------------------------------------------------------------- 1 | from .scored_item import ScoredItem, ScoredItemSet 2 | from .scored_item_tools import * 3 | from .scored_item_plot import * -------------------------------------------------------------------------------- /GGanalysis/ScoredItem/genshin_like_scored_item.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.ScoredItem.scored_item import ScoredItem 2 | from functools import lru_cache 3 | import itertools 4 | import numpy as np 5 | from typing import Callable 6 | 7 | def create_get_init_state(stats_weights=None, sub_stats_ranks=[7,8,9,10], rank_multi=1) -> Callable[[list[str], float], ScoredItem]: 8 | """返回计算 获得拥有指定初始副词条的道具初始得分分布,及该得分下每个副词条的期望占比 的函数""" 9 | @lru_cache(maxsize=65536) 10 | def get_init_state(stat_comb, default_weight=0): 11 | score_dist = np.zeros(40 * rank_multi + 1) 12 | sub_stat_exp = {} 13 | for sub_stat in stat_comb: 14 | sub_stat_exp[sub_stat] = np.zeros(40 * rank_multi + 1) 15 | # 生成所有可能的初始副词条组合 16 | stat_score_combinations = itertools.product(sub_stats_ranks, repeat=len(stat_comb)) 17 | for stat_score in stat_score_combinations: 18 | total_score = 0 19 | for score, stat in zip(stat_score, stat_comb): 20 | total_score += score * stats_weights.get(stat, default_weight) * rank_multi 21 | # 采用比例分配 22 | L = int(total_score) 23 | R = L + 1 24 | w_L = R - total_score 25 | w_R = total_score - L 26 | R = min(R, 40 * rank_multi) 27 | score_dist[L] += w_L 28 | score_dist[R] += w_R 29 | for score, stat in zip(stat_score, stat_comb): 30 | sub_stat_exp[stat][L] += score * w_L 31 | sub_stat_exp[stat][R] += score * w_R 32 | # 对于所有种情况进行归一化 并移除末尾的0,节省一点后续计算 33 | for sub_stat in stat_comb: 34 | sub_stat_exp[sub_stat] = np.divide( 35 | sub_stat_exp[sub_stat], 36 | score_dist, 37 | out=np.zeros_like(sub_stat_exp[sub_stat]), 38 | where=score_dist != 0, 39 | ) 40 | sub_stat_exp[sub_stat] = np.trim_zeros(sub_stat_exp[sub_stat], "b") 41 | score_dist /= len(sub_stats_ranks) ** len(stat_comb) 42 | score_dist = np.trim_zeros(score_dist, "b") 43 | return ScoredItem(score_dist, sub_stat_exp) 44 | return get_init_state 45 | 46 | def create_get_state_level_up(stats_weights=None, sub_stats_ranks=[7,8,9,10], rank_multi=1) -> Callable: 47 | """返回计算 这个函数计算在给定选择词条下升1级的分数分布及每个分数下不同副词条贡献期望占比 的函数""" 48 | @lru_cache(maxsize=65536) 49 | def get_state_level_up(stat_comb, default_weight=0): 50 | score_dist = np.zeros(10 * rank_multi + 1) 51 | sub_stat_exp = {} 52 | for stat in stat_comb: 53 | sub_stat_exp[stat] = np.zeros(10 * rank_multi + 1) 54 | # 枚举升级词条及词条数,共4*4=16种 (如果stat_comb数量不为4则有不同) 55 | for stat in stat_comb: 56 | for j in sub_stats_ranks: 57 | score = stats_weights.get(stat, default_weight) * j * rank_multi 58 | # 采用比例分配 59 | L = int(score) 60 | R = L + 1 61 | w_L = R - score 62 | w_R = score - L 63 | R = min(R, 10 * rank_multi) 64 | # 记录数据 65 | score_dist[L] += w_L 66 | sub_stat_exp[stat][L] += j * w_L 67 | score_dist[R] += w_R 68 | sub_stat_exp[stat][R] += j * w_R 69 | # 对于各种组合情况进行归一化 并移除末尾的0,节省一点后续计算 70 | for stat in stat_comb: 71 | sub_stat_exp[stat] = np.divide( 72 | sub_stat_exp[stat], 73 | score_dist, 74 | out=np.zeros_like(sub_stat_exp[stat]), 75 | where=score_dist != 0, 76 | ) 77 | sub_stat_exp[stat] = np.trim_zeros(sub_stat_exp[stat], "b") 78 | score_dist /= len(sub_stats_ranks) * len(stat_comb) 79 | score_dist = np.trim_zeros(score_dist, "b") 80 | return ScoredItem(score_dist, sub_stat_exp) 81 | return get_state_level_up -------------------------------------------------------------------------------- /GGanalysis/ScoredItem/scored_item.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from itertools import permutations, combinations 3 | from copy import deepcopy 4 | import time 5 | 6 | ''' 7 | 词条评分型道具类 8 | 参考 ideless 的思想,将难以处理的多个不同类别的属性,通过线性加权进行打分变成一维问题 9 | 本模块中部分计算方法在ideless的基础上重新实现并抽象 10 | ideless 原仓库见 https://github.com/ideless/reliq 11 | ''' 12 | 13 | class ScoredItem(): 14 | '''词条型道具''' 15 | # TODO 研究怎样继承 stats_score 比较好,运算时 stats_score 应该一致才能运算 套装 stats_score 也应该一致 16 | def __init__(self, score_dist: Union[FiniteDist, np.ndarray, list]=FiniteDist([0]), sub_stats_exp: dict={}, drop_p=1, stats_score: dict={}) -> None: 17 | '''使用分数分布和副词条期望完成初始化,可选副词条方差''' 18 | self.score_dist = FiniteDist(score_dist) # 得分分布 19 | self.stats_score = stats_score 20 | self.sub_stats_exp = sub_stats_exp # 每种副词条在每个得分下的期望 21 | self.drop_p = drop_p 22 | if self.drop_p < 0 or self.drop_p > 1: 23 | raise ValueError("drop_p should between 0 and 1!") 24 | self.fit_sub_stats() 25 | self.null_mark = self.is_null() 26 | 27 | def fit_sub_stats(self): 28 | '''调整副词条平均值长度以适应分数分布长度''' 29 | for key in self.sub_stats_exp.keys(): 30 | if len(self.sub_stats_exp[key]) > self.__len__(): 31 | self.sub_stats_exp[key] = self.sub_stats_exp[key][:self.__len__()] 32 | else: 33 | self.sub_stats_exp[key] = pad_zero(self.sub_stats_exp[key], self.__len__()) 34 | 35 | def is_null(self): 36 | '''判断自身是否为空''' 37 | return np.sum(self.score_dist.dist) == 0 38 | 39 | # 以下模块被 scored_item_tools.get_mix_dist 调用 40 | def sub_stats_clear(self): 41 | '''清除score_dist为0对应的sub_stats_exp''' 42 | mask = self.score_dist.dist != 0 43 | for key in self.sub_stats_exp.keys(): 44 | self.sub_stats_exp[key] = self.sub_stats_exp[key] * mask 45 | 46 | def check_subexp_sum(self) -> np.ndarray: 47 | '''检查副词条的加权和''' 48 | ans = np.zeros(len(self)) 49 | for key in self.sub_stats_exp.keys(): 50 | ans[:len(self.sub_stats_exp[key])] += self.sub_stats_exp[key] * self.stats_score.get(key, 0) 51 | return ans 52 | 53 | def repeat(self, n: int=1, p=None) -> 'ScoredItem': 54 | ''' 55 | 重复n次获取道具尝试 56 | 每次有p概率获得道具后获得的最大值分布 57 | 若不指定p则按self.drop_p为掉落概率 58 | ''' 59 | if n == 0: 60 | return ScoredItem([1], stats_score=self.stats_score) 61 | if p is None: 62 | use_p = self.drop_p 63 | else: 64 | if p < 0 or p > 1: 65 | raise ValueError("p should between 0 and 1!") 66 | use_p = p 67 | cdf = (use_p * np.cumsum(self.score_dist.dist) + 1 - use_p) ** n 68 | return ScoredItem(cdf2dist(cdf), self.sub_stats_exp, stats_score=self.stats_score) 69 | 70 | def get_sub_stat_avg(self, key): 71 | return np.sum(self.sub_stats_exp[key] * self.score_dist.dist) 72 | 73 | def __getattr__(self, key): # 访问未计算的属性时进行计算 74 | # 基本统计属性 75 | if key in ['exp', 'var']: 76 | if key == 'exp': 77 | self.exp = self.score_dist.exp 78 | return self.exp 79 | if key == 'var': 80 | self.var = self.score_dist.var 81 | return self.var 82 | if key == 'stats_score': # 词条得分,其值应位于0-1之间,默认为空 83 | return {} 84 | 85 | def __getitem__(self, sliced): 86 | '''根据sliced形成遮罩,返回sliced选中区间的值,其他位置设置为0''' 87 | mask = np.zeros_like(self.score_dist.dist) 88 | mask[sliced] = 1 89 | score_dist = np.trim_zeros(self.score_dist.dist * mask, 'b') 90 | sub_stats_exp = {} 91 | for key in self.sub_stats_exp.keys(): 92 | sub_stats_exp[key] = self.sub_stats_exp[key] * mask 93 | return ScoredItem(FiniteDist(score_dist), sub_stats_exp, stats_score=self.stats_score) 94 | 95 | def __add__(self, other: 'ScoredItem') -> 'ScoredItem': 96 | '''数量合并两个物品,除了副词条贡献期望占比其他属性均线性相加''' 97 | # 判断 stats_score 是否一致,评分标准必须一致 98 | if self.stats_score != other.stats_score: 99 | raise ValueError("stats_score must be the same!") 100 | key_set = set(self.sub_stats_exp.keys()) 101 | key_set.update(other.sub_stats_exp.keys()) 102 | ans_dist = self.score_dist + other.score_dist 103 | ans_sub_stats_exp = {} 104 | target_len = max(len(self), len(other)) 105 | for key in key_set: 106 | s_exp = self.sub_stats_exp.get(key, np.zeros(1)) 107 | o_exp = other.sub_stats_exp.get(key, np.zeros(1)) 108 | # 此处要对副词条取平均值 109 | a = pad_zero(s_exp * self.score_dist.dist, target_len) + \ 110 | pad_zero(o_exp * other.score_dist.dist, target_len) 111 | b = ans_dist.dist[:target_len] 112 | ans_sub_stats_exp[key] = np.divide(a, b, \ 113 | out=np.zeros_like(a), where=b!=0) 114 | return ScoredItem(ans_dist, ans_sub_stats_exp, stats_score=self.stats_score) 115 | 116 | def __mul__(self, other: Union['ScoredItem', float, int]) -> 'ScoredItem': 117 | '''对两个物品进行卷积合并,或单纯数乘''' 118 | if isinstance(other, ScoredItem): 119 | # 判断 stats_score 是否一致,评分标准必须一致 120 | if self.stats_score != other.stats_score: 121 | raise ValueError("stats_score must be the same!") 122 | # 两者都是词条型道具的情况下,进行卷积合并 123 | new_score_dist = self.score_dist * other.score_dist 124 | new_sub_exp = {} 125 | key_set = set(self.sub_stats_exp.keys()) 126 | key_set.update(other.sub_stats_exp.keys()) 127 | for key in key_set: 128 | a = convolve(self.score_dist.dist * self.sub_stats_exp.get(key, np.zeros(1)), \ 129 | other.score_dist.dist) 130 | b = convolve(other.score_dist.dist * other.sub_stats_exp.get(key, np.zeros(1)), \ 131 | self.score_dist.dist) 132 | a = pad_zero(a, len(new_score_dist)) 133 | b = pad_zero(b, len(new_score_dist)) 134 | new_sub_exp[key] = np.divide((a + b), new_score_dist.dist, out=np.zeros_like(new_score_dist.dist), where=new_score_dist.dist!=0) 135 | return ScoredItem(new_score_dist, new_sub_exp, stats_score=self.stats_score) 136 | else: 137 | # other为常数的情况下,进行数乘,数乘下不影响副词条平均值 138 | new_score_dist = self.score_dist * other 139 | new_sub_exp = self.sub_stats_exp 140 | return ScoredItem(new_score_dist, new_sub_exp, stats_score=self.stats_score) 141 | 142 | def __rmul__(self, other: Union['ScoredItem', float, int]) -> 'ScoredItem': 143 | return self * other 144 | 145 | def __len__(self) -> int: 146 | return len(self.score_dist) 147 | 148 | def __str__(self) -> str: 149 | return f"Score Dist {self.score_dist.dist} Sub Exp {self.sub_stats_exp}" 150 | 151 | class ScoredItemSet(): 152 | '''由词条型道具组成的套装''' 153 | def __init__(self, item_set:dict={}) -> None: 154 | '''由词条型道具构成的套装抽象''' 155 | self.item_set = item_set 156 | 157 | def add_item(self, item_name:str, item: ScoredItem): 158 | '''添加名为 item_name 的道具''' 159 | self.item_set[item_name] = item 160 | 161 | def combine_set(self, n=1): 162 | ''' 163 | 计算套装内每个道具获取n次道具后套装中道具最佳得分和的分布 164 | 若道具有设定掉落概率则按设定掉落概率处理 165 | n较大时可以近似为这一套装总的得分分布 166 | ''' 167 | ans = ScoredItem([1], stats_score=list(self.item_set.values())[0].stats_score) 168 | for key in self.item_set.keys(): 169 | ans *= self.item_set[key].repeat(n) 170 | return ans 171 | 172 | def repeat(self, n, p: dict=None) -> list[ScoredItem]: 173 | '''重复n次获取道具尝试,返回重复后的道具组''' 174 | ans = [] 175 | for key in sorted(list(self.item_set.keys()), key=str.lower): 176 | if p is None: 177 | use_p = self.item_set[key].drop_p 178 | else: 179 | use_p = p[key] 180 | if use_p < 0 or use_p > 1: 181 | raise ValueError("use_p should between 0 and 1!") 182 | ans.append(self.item_set[key].repeat(n, use_p)) 183 | return ans 184 | 185 | def to_list(self): 186 | '''返回以列表形式储存的道具''' 187 | return list(self.item_set.values()) 188 | 189 | if __name__ == '__main__': 190 | pass -------------------------------------------------------------------------------- /GGanalysis/SimulationTools/scored_item_sim.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | from copy import deepcopy 4 | from GGanalysis.SimulationTools.statistical_tools import Statistics 5 | import multiprocessing 6 | from tqdm import tqdm 7 | 8 | class HoyoItemSim(): 9 | def __init__( 10 | self, 11 | stat_score, # 副词条评分 12 | item_type, # 道具类型 13 | main_stat, # 设定主词条 14 | W_MAIN_STAT, # 主词条权重 15 | W_SUB_STAT, # 副词条权重 16 | UPDATE_TIMES, # 后续可升级次数分布 17 | UPDATE_TICKS, # 词条属性数值分布 18 | ) -> None: 19 | self.stat_score = stat_score 20 | 21 | self.type = item_type 22 | self.main_stat = main_stat 23 | self.main_stat_weight = W_MAIN_STAT 24 | self.sub_stats_weight = deepcopy(W_SUB_STAT) 25 | if self.main_stat in self.sub_stats_weight: 26 | del self.sub_stats_weight[self.main_stat] 27 | # 从字典中提取键和权重 28 | self.sub_stat_keys = list(self.sub_stats_weight.keys()) 29 | # 归一化权重 30 | self.sub_stat_weights = list(self.sub_stats_weight.values()) 31 | total_weight = sum(self.sub_stat_weights) 32 | self.sub_stat_weights = [w / total_weight for w in self.sub_stat_weights] 33 | 34 | self.UPDATE_TIMES = UPDATE_TIMES 35 | self.UPDATE_TICKS = UPDATE_TICKS 36 | 37 | def sample(self): 38 | # 是否满足主词条 39 | if random.random() > self.main_stat_weight[self.type][self.main_stat] / sum(self.main_stat_weight[self.type].values()): 40 | return 0, {} 41 | # 选择副词条升级次数 42 | update_times = np.random.choice(range(len(self.UPDATE_TIMES)), p=self.UPDATE_TIMES) 43 | # 选择副词条 44 | selected_stats = np.random.choice(self.sub_stat_keys, size=4, replace=False, p=self.sub_stat_weights) 45 | # 确定副词条初始数值 46 | stat_value = {} 47 | for stat in selected_stats: 48 | stat_value[stat] = np.random.choice(range(len(self.UPDATE_TICKS)), p=self.UPDATE_TICKS) 49 | # 升级副词条数值(均匀分布) 50 | for i in range(update_times): 51 | stat = selected_stats[random.randint(0, 3)] 52 | stat_value[stat] += np.random.choice(range(len(self.UPDATE_TICKS)), p=self.UPDATE_TICKS) 53 | # 计算部位得分 54 | score = 0 55 | for stat in selected_stats: 56 | score += self.stat_score[stat] * stat_value[stat] 57 | return score, stat_value 58 | 59 | def sample_best(self, n): 60 | # 重复 n 次后的最好值 61 | best_score = 0 62 | best_stat_value = {} 63 | for i in range(n): 64 | score, stat_value = self.sample() 65 | if score > best_score: 66 | best_score = score 67 | best_stat_value = stat_value 68 | return best_score, best_stat_value 69 | 70 | class HoyoItemSetSim(): 71 | def __init__(self, items, items_p, set_p, stat_score) -> None: 72 | self.items:list[HoyoItemSim] = items 73 | self.items_p:list[float] = items_p 74 | self.set_p = set_p 75 | self.stat_score = stat_score 76 | 77 | def sample_best(self, n): 78 | set_n = np.random.binomial(n, self.set_p) 79 | item_nums = np.random.multinomial(set_n, self.items_p) 80 | score = 0 81 | stat_value = {} 82 | for num, item in zip(item_nums, self.items): 83 | item_score, item_stat = item.sample_best(num) 84 | score += item_score 85 | for key in item_stat.keys(): 86 | if key in stat_value: 87 | stat_value[key] += item_stat[key] 88 | else: 89 | stat_value[key] = item_stat[key] 90 | return score, stat_value 91 | 92 | def sim_player_group(self, player_num, n, record_sub_stat_dist=False): 93 | # 总共 player_num 个玩家,每个玩家获得 n 件道具 94 | score_record = Statistics(is_record_dist=True) 95 | stat_record = {} 96 | 97 | def record_sub_stat(stat_value): 98 | for key in self.stat_score.keys(): 99 | if key not in stat_record: 100 | stat_record[key] = Statistics(record_sub_stat_dist) 101 | stat_record[key].update(stat_value.get(key, 0)) 102 | for _ in range(player_num): 103 | score, stat_value = self.sample_best(n) 104 | score_record.update(score) 105 | record_sub_stat(stat_value) 106 | 107 | return score_record, stat_record 108 | 109 | def parallel_sim_player_group(self, player_num, n, forced_processes=None, record_sub_stat_dist=False): 110 | # 总共 player_num 个玩家,每个玩家获得 n 件道具 111 | num_processes = multiprocessing.cpu_count() 112 | if forced_processes is not None: 113 | num_processes = forced_processes 114 | 115 | with multiprocessing.Pool(num_processes) as pool: 116 | # 使用map方法将计算任务分发给所有进程 117 | results = list(tqdm(pool.imap(self.sample_best, [n]*player_num), total=player_num)) 118 | 119 | score_record = Statistics(is_record_dist=True) 120 | stat_record = {} 121 | def record_sub_stat(stat_value): 122 | for key in self.stat_score.keys(): 123 | if key not in stat_record: 124 | stat_record[key] = Statistics(record_sub_stat_dist) 125 | stat_record[key].update(stat_value.get(key, 0)) 126 | 127 | for score, stat_value in results: 128 | score_record.update(score) 129 | record_sub_stat(stat_value) 130 | # 返回处理后的结果 131 | return score_record, stat_record 132 | 133 | if __name__ == '__main__': 134 | from GGanalysis.games.honkai_star_rail.relic_data import W_MAIN_STAT,W_SUB_STAT,CAVERN_RELICS,PLANAR_ORNAMENTS,DEFAULT_MAIN_STAT,DEFAULT_STAT_SCORE 135 | test_item = HoyoItemSim( 136 | DEFAULT_STAT_SCORE, 137 | CAVERN_RELICS[0], 138 | list(W_MAIN_STAT[CAVERN_RELICS[0]].keys())[0], 139 | W_MAIN_STAT, 140 | W_SUB_STAT, 141 | [0,0,0,0.8,0.2], 142 | [0,0,0,0,0,0,0,0,1/3,1/3,1/3], 143 | ) 144 | # print(test_item.sample()) 145 | # print(test_item.sample_best(1000)) 146 | 147 | test_item_set = HoyoItemSetSim([test_item], [1], 1/2, DEFAULT_STAT_SCORE) 148 | # print(test_item_set.sample_best(1000)) 149 | import time 150 | t0 = time.time() 151 | print(test_item_set.sim_player_group(100, 1000)) 152 | t1 = time.time() 153 | print(test_item_set.parallel_sim_player_group(100, 1000, 16)) 154 | t2 = time.time() 155 | print(t1-t0, t2-t1, "Rate =", (t1-t0)/(t2-t1)) -------------------------------------------------------------------------------- /GGanalysis/SimulationTools/statistical_tools.py: -------------------------------------------------------------------------------- 1 | class Statistics(): 2 | ''' 3 | 统计记录类 4 | ''' 5 | def __init__(self, is_record_dist=False) -> None: 6 | ''' 7 | 使用 Welford 方法更新均值与方差 8 | 默认不记录分布,若需要记录分布则使用字典记录(等效于散列表) 9 | ''' 10 | self.count = 0 # 数据数量 11 | self.mean = 0 12 | self.M2 = 0 13 | self.max = None 14 | self.min = None 15 | # 分布记录 16 | self.is_record_dist = is_record_dist 17 | if is_record_dist: 18 | self.dist = {} 19 | def update(self, x): 20 | '''记录新加入记录并更新当前统计量''' 21 | self.count += 1 22 | if self.max is None or x > self.max: 23 | self.max = x 24 | if self.min is None or x < self.min: 25 | self.min = x 26 | # 中间值更新 27 | delta = x - self.mean 28 | self.mean += delta / self.count 29 | delta2 = x - self.mean 30 | self.M2 += delta * delta2 31 | # 分布记录 32 | if self.is_record_dist: 33 | if x in self.dist: 34 | self.dist[x] += 1 35 | else: 36 | self.dist[x] = 1 37 | 38 | def __str__(self) -> str: 39 | return f"(mean={self.mean:.4f}, svar={self.svar:.4f})" 40 | def __repr__(self): 41 | return self.__str__() 42 | 43 | def __getattr__(self, key): 44 | if key == "var": 45 | if self.count < 2: 46 | return float('nan') 47 | return self.M2 / self.count 48 | elif key == "svar": 49 | if self.count < 2: 50 | return float('nan') 51 | return self.M2 / (self.count-1) 52 | elif key == "std": 53 | return (self.__getattr__("var"))**0.5 54 | else: 55 | raise AttributeError(f"{type(self).__name__!r} object has no attribute {key!r}") 56 | def __add__(self, other): 57 | if not isinstance(other, Statistics): 58 | return NotImplemented 59 | result = Statistics(is_record_dist=self.is_record_dist and other.is_record_dist) 60 | if self.max is not None and other.max is not None: 61 | result.max = max(self.max, other.max) 62 | result.min = min(self.min, other.min) 63 | else: 64 | if self.max is not None: 65 | result.max = self.max 66 | result.min = self.min 67 | else: 68 | result.max = other.max 69 | result.min = other.min 70 | # 使用 Chan, Golub, and LeVeque 提出的方法进行合并 71 | result.count = self.count + other.count 72 | delta = other.mean - self.mean 73 | weighted_mean = self.mean + delta * other.count / result.count 74 | result.mean = weighted_mean 75 | result.M2 = self.M2 + other.M2 + delta**2 * self.count * other.count / result.count 76 | # 分布合并 77 | if result.is_record_dist: 78 | result.dist = self.dist.copy() 79 | for key, value in other.dist.items(): 80 | if key in result.dist: 81 | result.dist[key] += value 82 | else: 83 | result.dist[key] = value 84 | return result 85 | 86 | if __name__ == '__main__': 87 | import numpy as np 88 | 89 | # 测试单个统计对象 90 | data = np.random.rand(100) 91 | stats = Statistics() 92 | for number in data: 93 | stats.update(number) 94 | 95 | print("Statistics mean:", stats.mean) 96 | print("Numpy mean:", np.mean(data)) 97 | print("Diff", stats.mean-np.mean(data)) 98 | print("Statistics variance:", stats.svar) 99 | print("Numpy variance:", np.var(data, ddof=1)) 100 | print("Diff", stats.svar-np.var(data, ddof=1)) 101 | print("min max", stats.min, np.min(data), stats.max, np.max(data)) 102 | 103 | # 测试两个统计对象的合并 104 | data1 = np.random.rand(100) 105 | data2 = np.random.rand(100) 106 | stats1 = Statistics() 107 | stats2 = Statistics() 108 | for number in data1: 109 | stats1.update(number) 110 | for number in data2: 111 | stats2.update(number) 112 | 113 | combined_stats = stats1 + stats2 114 | combined_data = np.concatenate((data1, data2)) 115 | 116 | print("Combined Statistics mean:", combined_stats.mean) 117 | print("Combined Numpy mean:", np.mean(combined_data)) 118 | print("Diff", combined_stats.mean-np.mean(combined_data)) 119 | print("Combined Statistics variance:", combined_stats.svar) 120 | print("Combined Numpy variance:", np.var(combined_data, ddof=1)) 121 | print("Diff", combined_stats.svar-np.var(combined_data, ddof=1)) 122 | print("min max", combined_stats.min, np.min(combined_data), combined_stats.max, np.max(combined_data)) -------------------------------------------------------------------------------- /GGanalysis/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 抽卡游戏抽卡概率计算工具包 GGanalysis 3 | by 一棵平衡树OneBST 4 | ''' 5 | from GGanalysis.distribution_1d import * 6 | from GGanalysis.gacha_layers import * 7 | from GGanalysis.basic_models import * 8 | from GGanalysis.coupon_collection import * 9 | from GGanalysis.markov_method import * -------------------------------------------------------------------------------- /GGanalysis/basic_models.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.coupon_collection import GeneralCouponCollection 4 | from typing import Union 5 | 6 | class GachaModel(object): 7 | '''所有抽卡模型的基类''' 8 | pass 9 | 10 | class CommonGachaModel(GachaModel): 11 | '''基本抽卡类 对每次获取道具是独立事件的抽象''' 12 | def __init__(self) -> None: 13 | super().__init__() 14 | # 初始化抽卡层 15 | self.layers = [] 16 | # 在本层中定义抽卡层 17 | 18 | def __call__(self, item_num: int=1, multi_dist: bool=False, *args: any, **kwds: any) -> Union[FiniteDist, list]: 19 | '''调用本类时返回分布''' 20 | parameter_list = self._build_parameter_list(*args, **kwds) 21 | # 如果没有对 _build_parameter_list 进行定义就输入参数,报错 22 | if args != () and kwds != {} and parameter_list is None: 23 | raise Exception('Parameters is not defined.') 24 | # 如果 item_num 为 0,则返回 1 分布 25 | if item_num == 0: 26 | return FiniteDist([1]) 27 | # 如果 multi_dist 参数为真,返回抽取 [1, 抽取个数] 个道具的分布列表 28 | if multi_dist: 29 | return self._get_multi_dist(item_num, parameter_list) 30 | # 其他情况正常返回 31 | return self._get_dist(item_num, parameter_list) 32 | 33 | # 用于输入参数解析,生成每层对应参数列表 34 | def _build_parameter_list(self, *args: any, **kwds: any) -> list: 35 | parameter_list = [] 36 | for i in range(len(self.layers)): 37 | parameter_list.append([[], {}]) 38 | return parameter_list 39 | 40 | # 输入 [完整分布, 条件分布] 指定抽取个数,返回抽取 [1, 抽取个数] 个道具的分布列表 41 | def _get_multi_dist(self, end_pos: int, parameter_list: list=None): 42 | input_dist = self._forward(parameter_list) 43 | ans_list = [FiniteDist([1]), input_dist[1]] 44 | for i in range(1, end_pos): 45 | # 添加新的一层并设定方差与期望 46 | ans_list.append(ans_list[i] * input_dist[0]) 47 | ans_list[i+1].exp = input_dist[1].exp + input_dist[0].exp * i 48 | ans_list[i+1].var = input_dist[1].var + input_dist[0].var * i 49 | return ans_list 50 | 51 | # 返回单个分布 52 | def _get_dist(self, item_num: int, parameter_list: list=None): 53 | ans_dist = self._forward(parameter_list) 54 | ans: FiniteDist = ans_dist[1] * ans_dist[0] ** (item_num - 1) 55 | ans.exp = ans_dist[1].exp + ans_dist[0].exp * (item_num - 1) 56 | ans.var = ans_dist[1].var + ans_dist[0].var * (item_num - 1) 57 | return ans 58 | 59 | # 根据多层信息计算获取物品分布列 60 | def _forward(self, parameter_list: list=None): 61 | ans_dist = None 62 | # 没有输入参数返回默认分布 63 | if parameter_list is None: 64 | for layer in self.layers: 65 | ans_dist = layer(ans_dist) 66 | return ans_dist 67 | # 有输入参数则将分布逐层推进 68 | for parameter, layer in zip(parameter_list, self.layers): 69 | ans_dist = layer(ans_dist, *parameter[0], **parameter[1]) 70 | return ans_dist 71 | 72 | class BernoulliGachaModel(GachaModel): 73 | '''伯努利抽卡类''' 74 | def __init__(self, p, e_error = 1e-8, max_dist_len=1e5) -> None: 75 | super().__init__() 76 | self.p = p # 伯努利试验概率 77 | self.e_error = e_error 78 | self.max_dist_len = max_dist_len 79 | 80 | def __call__(self, item_num: int, calc_pull: int=None) -> FiniteDist: 81 | ''' 82 | 返回抽物品个数的分布 83 | 这里 calc_pull 表示了计算的最高抽数,高于此不计算,若不指定则返回自动长度 84 | ''' 85 | output_E = item_num / self.p 86 | output_D = item_num * (1 - self.p) / self.p ** 2 87 | if calc_pull is None: 88 | test_len = max(int(output_E), 2) 89 | while True: 90 | x = np.arange(test_len+1) 91 | output_dist = self.p * (binom.pmf(item_num-1, x-1, self.p)) 92 | output_dist[0] = 0 93 | output_dist = FiniteDist(output_dist) 94 | calc_error = abs(calc_expectation(output_dist)-output_E)/output_E 95 | if calc_error < self.e_error or test_len > self.max_dist_len: 96 | if test_len > self.max_dist_len: 97 | print('Warning: distribution is too long! len:', test_len, 'Error:', calc_error) 98 | output_dist.exp = output_E 99 | output_dist.var = output_D 100 | return output_dist 101 | test_len *= 2 102 | # 指定长度时不内嵌理论方差和期望 103 | x = np.arange(calc_pull+1) 104 | output_dist = self.p * (binom.pmf(item_num-1, x-1, self.p)) 105 | output_dist[0] = 0 106 | output_dist = FiniteDist(output_dist) 107 | return output_dist 108 | 109 | # 计算固定抽数,获得道具数的分布 110 | ''' 111 | def _get_dist(self, item_num, pulls): # 恰好在第x抽抽到item_num个道具的概率,限制长度最高为pulls 112 | x = np.arange(pulls+1) 113 | dist = self.p * (binom.pmf(0, x-1, self.p)) 114 | dist[0] = 0 115 | return finite_dist_1D(dist) 116 | ''' 117 | 118 | class CouponCollectorModel(CommonGachaModel): 119 | '''均等概率集齐道具抽卡类''' 120 | def __init__(self, item_types, e_error = 1e-6, max_dist_len = 1e5) -> None: 121 | super().__init__() 122 | self.layers.append(CouponCollectorLayer(item_types, None, e_error, max_dist_len)) 123 | 124 | def __call__(self, initial_types: int = 0, target_types: int = None) -> Union[FiniteDist, list]: 125 | return super().__call__(1, False, initial_types, target_types) 126 | 127 | def _build_parameter_list(self, initial_types: int = 0, target_types: int = None) -> list: 128 | parameter_list = [ 129 | [[], {'initial_types':initial_types, 'target_types':target_types}], 130 | ] 131 | return parameter_list 132 | 133 | class PityCouponCollectorModel(CommonGachaModel): 134 | '''道具保底均等概率集齐道具抽卡类''' 135 | def __init__(self, pity_p, item_types, e_error = 1e-6, max_dist_len = 1e5) -> None: 136 | super().__init__() 137 | self.layers.append(PityLayer(pity_p)) 138 | self.layers.append(CouponCollectorLayer(item_types, None, e_error, max_dist_len)) 139 | 140 | def __call__(self, initial_types: int = 0, item_pity = 0, target_types: int = None) -> Union[FiniteDist, list]: 141 | return super().__call__(1, False, item_pity, initial_types, target_types) 142 | 143 | def _build_parameter_list(self, item_pity: int=0, initial_types: int = 0, target_types: int = None) -> list: 144 | parameter_list = [ 145 | [[], {'item_pity':item_pity}], 146 | [[], {'initial_types':initial_types, 'target_types':target_types}], 147 | ] 148 | return parameter_list 149 | 150 | class DualPityCouponCollectorModel(CommonGachaModel): 151 | '''道具保底均等概率集齐道具抽卡类''' 152 | def __init__(self, pity_p1, pity_p2, item_types, e_error = 1e-6, max_dist_len = 1e5) -> None: 153 | super().__init__() 154 | self.layers.append(PityLayer(pity_p1)) 155 | self.layers.append(PityLayer(pity_p2)) 156 | self.layers.append(CouponCollectorLayer(item_types, None, e_error, max_dist_len)) 157 | 158 | def __call__(self, initial_types: int = 0, item_pity = 0, up_pity: int = 0, target_types: int = None) -> Union[FiniteDist, list]: 159 | return super().__call__(1, False, item_pity, up_pity, initial_types, target_types) 160 | 161 | def _build_parameter_list(self, item_pity: int = 0, up_pity:int = 0, initial_types: int = 0, target_types: int = None) -> list: 162 | parameter_list = [ 163 | [[], {'item_pity':item_pity}], 164 | [[], {'item_pity':up_pity}], 165 | [[], {'initial_types':initial_types, 'target_types':target_types}], 166 | ] 167 | return parameter_list 168 | 169 | class GeneralCouponCollectorModel(GachaModel): 170 | '''不均等概率集齐道具抽卡类''' 171 | def __init__(self, p_list: Union[list, np.ndarray], item_name: list[str]=None, e_error = 1e-6, max_dist_len = 1e5) -> None: 172 | super().__init__() 173 | self.e_error = e_error 174 | self.max_dist_len = max_dist_len 175 | self.model = GeneralCouponCollection(p_list, item_name) 176 | 177 | def __call__(self, init_item: list=None, target_item: list=None) -> FiniteDist: 178 | # 输入处理 179 | if init_item is None: 180 | init_state = self.model.default_init_state 181 | else: 182 | init_state = self.model.encode_state_number(init_item) 183 | if target_item is None: 184 | target_state = self.model.default_target_state 185 | else: 186 | target_state = self.model.encode_state_number(target_item) 187 | output_E = self.model.get_expectation(init_state, target_state) 188 | test_len = max(int(output_E), 2) 189 | while True: 190 | output_dist = cdf2dist(self.model.get_collection_p(test_len, init_state, target_state)) 191 | calc_error = abs(calc_expectation(output_dist)-output_E)/output_E 192 | if calc_error < self.e_error or test_len > self.max_dist_len: 193 | if test_len > self.max_dist_len: 194 | print('Warning: distribution is too long! len:', test_len, 'Error:', calc_error) 195 | output_dist.exp = output_E 196 | return output_dist 197 | test_len *= 2 198 | 199 | class PityModel(CommonGachaModel): 200 | '''带保底抽卡类''' 201 | def __init__(self, pity_p) -> None: 202 | super().__init__() 203 | self.layers.append(PityLayer(pity_p)) 204 | 205 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity = 0) -> Union[FiniteDist, list]: 206 | return super().__call__(item_num, multi_dist, item_pity) 207 | 208 | def _build_parameter_list(self, item_pity: int=0) -> list: 209 | parameter_list = [[[], {'item_pity':item_pity}]] 210 | return parameter_list 211 | 212 | class DualPityModel(CommonGachaModel): 213 | '''双重保底抽卡类''' 214 | def __init__(self, pity_p1, pity_p2) -> None: 215 | super().__init__() 216 | self.layers.append(PityLayer(pity_p1)) 217 | self.layers.append(PityLayer(pity_p2)) 218 | 219 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity = 0, up_pity = 0) -> Union[FiniteDist, list]: 220 | return super().__call__(item_num, multi_dist, item_pity, up_pity) 221 | 222 | def _build_parameter_list(self, item_pity: int=0, up_pity: int=0) -> list: 223 | parameter_list = [ 224 | [[], {'item_pity':item_pity}], 225 | [[], {'item_pity':up_pity}], 226 | ] 227 | return parameter_list 228 | 229 | class PityBernoulliModel(CommonGachaModel): 230 | '''保底伯努利抽卡类''' 231 | def __init__(self, pity_p, p, e_error = 1e-8, max_dist_len=1e5) -> None: 232 | super().__init__() 233 | self.layers.append(PityLayer(pity_p)) 234 | self.layers.append(BernoulliLayer(p, e_error, max_dist_len)) 235 | 236 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity=0) -> Union[FiniteDist, list]: 237 | return super().__call__(item_num, multi_dist, item_pity) 238 | 239 | def _build_parameter_list(self, item_pity: int=0) -> list: 240 | parameter_list = [ 241 | [[], {'item_pity':item_pity}], 242 | [[], {}], 243 | ] 244 | return parameter_list 245 | 246 | class DualPityBernoulliModel(CommonGachaModel): 247 | '''双重保底伯努利类''' 248 | def __init__(self, pity_p1, pity_p2, p, e_error = 1e-8, max_dist_len=1e5) -> None: 249 | super().__init__() 250 | self.layers.append(PityLayer(pity_p1)) 251 | self.layers.append(PityLayer(pity_p2)) 252 | self.layers.append(BernoulliLayer(p, e_error, max_dist_len)) 253 | 254 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity = 0, up_pity = 0) -> Union[FiniteDist, list]: 255 | return super().__call__(item_num, multi_dist, item_pity, up_pity) 256 | 257 | def _build_parameter_list(self, item_pity: int=0, up_pity: int=0) -> list: 258 | parameter_list = [ 259 | [[], {'item_pity':item_pity}], 260 | [[], {'item_pity':up_pity}], 261 | [[], {}], 262 | ] 263 | return parameter_list -------------------------------------------------------------------------------- /GGanalysis/coupon_collection.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Union 3 | from functools import lru_cache 4 | import random 5 | 6 | class GeneralCouponCollection(): 7 | ''' 8 | 不同奖券概率不均等的奖券集齐问题类 9 | 默认以集齐为吸收态,状态按照从低到高位对应输入列表的从前往后 10 | ''' 11 | def __init__(self, p_list: Union[list, np.ndarray], item_names: list[str]=None) -> None: 12 | # 输入合法性检查 13 | # TODO 考虑是否添加多个吸收态的功能,还是出于速度考虑用DP代替这部分功能 14 | if len(p_list) >= 16: 15 | # 默认最高道具种数为 16 种,以防状态过多 16 | raise ValueError("Item types must below 16") 17 | sum_p = 0 18 | for p in p_list: 19 | if p <= 0: 20 | raise ValueError("Each p must larger than 0!") 21 | sum_p += p 22 | if sum_p > 1: 23 | raise ValueError("Sum P is greater than 1!") 24 | if item_names is not None: 25 | if len(item_names) != len(p_list): 26 | raise ValueError("item_name must have equal len with p_list!") 27 | # 类赋值 28 | self.p_list = np.array(p_list) # 目标物品的概率,其概率和可以小于1 29 | self.item_names = item_names # 道具名称 考虑用字典存其编号 30 | if item_names is not None: 31 | # 建立编号查找表,按照列表中的顺序进行编号 32 | self.item_dict = {} 33 | for i, item in enumerate(self.item_names): 34 | self.item_dict[item] = i 35 | self.fail_p = 1 - sum(p_list) # 不是目标中的任意一个的概率 36 | self.item_types = len(p_list) 37 | self.default_init_state = 0 # 默认起始状态为所有种类都没有 38 | self.default_target_state = 2 ** self.item_types - 1 # 默认目标状态为集齐状态 39 | 40 | def set_default_init_state(self, init_state): 41 | # 设置默认起始状态 42 | self.default_init_state = init_state 43 | 44 | def set_default_target_state(self, target_state): 45 | # 设置默认目标状态 46 | self.default_target_state = target_state 47 | 48 | def encode_state_number(self, item_list): 49 | '''根据道具列表生成对应状态''' 50 | ans = 0 51 | for item in item_list: 52 | ans = ans | (1 << self.item_dict[item]) 53 | return ans 54 | 55 | def decode_state_number(self, state_num): 56 | '''根据对应状态返回状态道具列表''' 57 | ans = [] 58 | for i in range(self.item_types): 59 | if state_num & (1 << i): 60 | ans.append(self.item_names[i]) 61 | return ans 62 | 63 | def get_satisfying_state(self, target_state=None): 64 | '''返回一个标记了满足目标状态要求的01numpy数组''' 65 | if target_state is None: 66 | target_state = self.default_target_state 67 | ans = np.arange(2 ** self.item_types, dtype=int) 68 | ans = (ans & target_state) == target_state 69 | return ans.astype(int) 70 | 71 | @lru_cache(maxsize=int(65536)) 72 | def get_expectation(self, state=None, target_state=None): 73 | '''带缓存递归计算抽取奖券到目标态时抽取数量期望''' 74 | if state is None: 75 | state = self.default_init_state 76 | if target_state is None: 77 | target_state = self.default_target_state 78 | if (state & target_state) == target_state: 79 | # 状态覆盖了目标状态 80 | return 0 81 | stay_p = self.fail_p 82 | temp = 0 83 | for i, p in enumerate(self.p_list): 84 | # 枚举本次抽到的奖券 85 | next_state = state | (1 << i) 86 | if next_state == state: 87 | # 这里必须是 += 抽到多个情况都可能保持在原地 88 | stay_p += p 89 | continue 90 | temp += p * self.get_expectation(next_state, target_state) 91 | return (1+temp) / (1-stay_p) 92 | 93 | def collection_dp(self, n, init_state=None): 94 | '''通过DP计算抽n次后的状态分布,返回DP数组''' 95 | if init_state is None: 96 | init_state = self.default_init_state 97 | M = np.zeros((self.default_target_state+1, n+1)) 98 | M[init_state, 0] = 1 99 | for t in range(n): 100 | for current_state in range(self.default_target_state+1): 101 | M[current_state, t+1] += M[current_state, t] * self.fail_p 102 | for i, p in enumerate(self.p_list): 103 | next_state = current_state | (1 << i) 104 | M[next_state, t+1] += M[current_state, t] * p 105 | return M 106 | 107 | def get_collection_p(self, n, init_state=None, target_state=None, DP_array=None): 108 | '''返回抽n抽后达到目标状态的概率数组''' 109 | if init_state is None: 110 | init_state = self.default_init_state 111 | if target_state is None: 112 | target_state = self.default_target_state 113 | satisfying_states = self.get_satisfying_state(target_state) 114 | if DP_array is not None: 115 | return satisfying_states.dot(DP_array) 116 | return satisfying_states.dot(self.collection_dp(n, init_state)) 117 | 118 | def sim_collection(self, state=None): 119 | '''蒙特卡洛模拟抽取次数,用于验证''' 120 | if state is None: 121 | state = self.default_init_state 122 | counter = 0 123 | while((state & self.default_target_state) != self.default_target_state): 124 | counter += 1 125 | rand_num = random.random() 126 | for i, p in enumerate(self.p_list): 127 | if rand_num < p: 128 | state = state | (1 << i) 129 | break 130 | rand_num -= p 131 | return counter 132 | 133 | def get_equal_coupon_collection_exp(item_type, init_type=0, target_type=None): 134 | '''获得每样道具均等情况下的集齐期望''' 135 | ans = 0 136 | if target_type is None: 137 | target_type = item_type 138 | if target_type > item_type: 139 | raise ValueError("target_type can't be greater than item_types!") 140 | if init_type < 0: 141 | raise ValueError("init_type can't below 0!") 142 | for i in range(init_type, target_type): 143 | ans += item_type/(item_type-i) 144 | return ans 145 | 146 | if __name__ == '__main__': 147 | pass -------------------------------------------------------------------------------- /GGanalysis/games/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /GGanalysis/games/alchemy_stars/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/alchemy_stars/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.alchemy_stars as AS 2 | from GGanalysis.gacha_plot import QuantileFunction, DrawDistribution 3 | from GGanalysis import FiniteDist 4 | import matplotlib.cm as cm 5 | import numpy as np 6 | import time 7 | 8 | def AS_character(x): 9 | return '抽取'+str(x)+'个' 10 | 11 | # 白夜极光 UP6星光灵 12 | AS_fig = QuantileFunction( 13 | AS.up_6star(5, multi_dist=True), 14 | title='白夜极光UP六星光灵抽取概率', 15 | item_name='UP六星光灵', 16 | text_head='采用官方公示模型\n获取1个UP六星光灵最多270抽', 17 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 18 | max_pull=1400, 19 | mark_func=AS_character, 20 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 5+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 21 | y_base_gap=25, 22 | y2x_base=2, 23 | is_finite=True) 24 | AS_fig.show_figure(dpi=300, savefig=True) 25 | 26 | # 白夜极光 获取6星光灵 27 | AS_fig = DrawDistribution( 28 | dist_data=AS.common_6star(1), 29 | title='白夜极光获取六星光灵', 30 | text_head='采用官方公示模型', 31 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 32 | item_name='六星光灵', 33 | is_finite=True, 34 | ) 35 | AS_fig.show_dist(dpi=300, savefig=True) 36 | 37 | # 白夜极光 获取UP6星光灵 38 | AS_fig = DrawDistribution( 39 | dist_data=AS.up_6star(1), 40 | title='白夜极光获取UP六星光灵', 41 | text_head='采用官方公示模型\n获取UP6星光灵最多需要抽3个六星\n保底进度跨池继承', 42 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 43 | item_name='UP六星光灵', 44 | description_pos=180, 45 | is_finite=True, 46 | ) 47 | AS_fig.show_dist(dpi=300, savefig=True) -------------------------------------------------------------------------------- /GGanalysis/games/alchemy_stars/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | # base on https://alchemystars.fandom.com/wiki/Recruitment and publicity in the chinese server 6 | __all__ = [ 7 | 'PITY_6STAR', 8 | 'P_5', 9 | 'P_4', 10 | 'P_3', 11 | 'P_6s', 12 | 'P_5s', 13 | 'P_4s', 14 | 'P_3s', 15 | 'common_6star', 16 | 'common_5star', 17 | 'common_4star', 18 | 'common_3star', 19 | 'up_6star', 20 | ] 21 | 22 | # 白夜极光6星保底概率表 23 | PITY_6STAR = np.zeros(91) 24 | PITY_6STAR[1:51] = 0.02 25 | PITY_6STAR[51:91] = np.arange(1, 41) * 0.025 + 0.02 26 | PITY_6STAR[90] = 1 27 | # 其他无保底物品初始概率 28 | P_5 = 0.095 29 | P_4 = 0.33 30 | P_3 = 0.555 31 | # 按照 stationary_p.py 的计算结果修订 32 | P_6s = 0.02914069 33 | P_5s = 0.095 34 | P_4s = 0.3299999 35 | P_3s = 0.54585941 36 | # 定义获取星级物品的模型 37 | common_6star = PityModel(PITY_6STAR) 38 | common_5star = BernoulliGachaModel(P_5s) 39 | common_4star = BernoulliGachaModel(P_4s) 40 | common_3star = BernoulliGachaModel(P_3s) 41 | # 定义获取UP物品模型 42 | up_6star = DualPityModel(PITY_6STAR, [0, 0.5, 0.5, 1]) 43 | 44 | if __name__ == '__main__': 45 | print(common_6star(1)[90]) 46 | print(up_6star(1)[270]) 47 | pass -------------------------------------------------------------------------------- /GGanalysis/games/alchemy_stars/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.alchemy_stars import PITY_6STAR, P_5, P_4, P_3 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具 5 | gacha_system = PriorityPitySystem([PITY_6STAR, [0, P_5], [0, P_4], [0, P_3]]) 6 | print(gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/arknights/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/arknights/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.arknights as AK 2 | from GGanalysis.gacha_plot import QuantileFunction 3 | import matplotlib.cm as cm 4 | import numpy as np 5 | import time 6 | 7 | def AK_character(x): 8 | return '潜能'+str(x) 9 | 10 | # 明日方舟6星角色 11 | AK_fig = QuantileFunction( 12 | AK.common_6star(10, multi_dist=True), 13 | title='明日方舟六星角色抽取概率', 14 | item_name='六星角色', 15 | text_head='请注意试绘图使用模型未必准确', 16 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 17 | max_pull=600, 18 | line_colors=0.5*(cm.Blues(np.linspace(0.1, 1, 10+1))+cm.Greys(np.linspace(0.4, 1, 10+1))), 19 | y2x_base=1.6, 20 | is_finite=True) 21 | AK_fig.show_figure(dpi=300, savefig=True) 22 | 23 | # 明日方舟普池单UP6星角色 24 | AK_fig = QuantileFunction( 25 | AK.single_up_6star(6, multi_dist=True), 26 | title='明日方舟普池单UP六星角色抽取概率', 27 | item_name='UP角色(定向选调)', 28 | text_head='UP角色占六星中50%概率\n请注意试绘图使用模型未必准确\n考虑定向选调机制(类型保底)\n获取第1个UP角色最多249抽\n无法确保在有限抽数内一定满潜', 29 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 30 | max_pull=1000, 31 | mark_func=AK_character, 32 | line_colors=cm.GnBu(np.linspace(0.3, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 33 | y_base_gap=25, 34 | is_finite=None) 35 | AK_fig.show_figure(dpi=300, savefig=True) 36 | 37 | # 明日方舟普池双UP6星角色 38 | AK_fig = QuantileFunction( 39 | AK.dual_up_specific_6star(6, multi_dist=True), 40 | title='明日方舟普池双UP获取特定六星角色概率', 41 | item_name='特定UP角色(第1个)', 42 | text_head='两个角色各占六星中25%概率\n请注意试绘图使用模型未必准确\n获取第1个UP角色最多501抽\n无法确保在有限抽数内一定满潜', 43 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 44 | max_pull=2200, 45 | mark_func=AK_character, 46 | line_colors=cm.PuRd(np.linspace(0.2, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 47 | y2x_base=1.8, 48 | is_finite=None) 49 | AK_fig.show_figure(dpi=300, savefig=True) 50 | 51 | # 明日方舟限定UP6星角色 52 | AK_fig = QuantileFunction( 53 | AK.limited_up_6star(6, multi_dist=True), 54 | title='明日方舟获取限定六星角色概率(考虑300井)', 55 | item_name='限定六星角色', 56 | text_head='两个角色各占六星中35%概率\n请注意试绘图使用模型未必准确', 57 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 58 | max_pull=1100, 59 | mark_func=AK_character, 60 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 61 | direct_exchange=300, 62 | y_base_gap=25, 63 | y2x_base=1.8, 64 | plot_direct_exchange=True, 65 | is_finite=False) 66 | AK_fig.show_figure(dpi=300, savefig=True) 67 | 68 | # 明日方舟限定UP6星角色 69 | AK_fig = QuantileFunction( 70 | AK.limited_up_6star(6, multi_dist=True), 71 | title='明日方舟获取限定六星角色概率(不考虑300井)', 72 | item_name='限定六星角色', 73 | text_head='两个角色各占六星中35%概率\n请注意试绘图使用模型未必准确', 74 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 75 | max_pull=1400, 76 | mark_func=AK_character, 77 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 78 | y_base_gap=25, 79 | y2x_base=1.8, 80 | is_finite=False) 81 | AK_fig.show_figure(dpi=300, savefig=True) 82 | -------------------------------------------------------------------------------- /GGanalysis/games/arknights/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | ''' 6 | 注意,本模块按公示概率进行建模,但忽略了一些情况 7 | 如不纳入对300保底的考虑,获取1个物品的分布不会在第300抽处截断 8 | 这么做的原因是,模型只支持一抽最多获取1个物品,若在第300抽处刚好抽到物品 9 | 等于一抽抽到两个物品,无法处理。 10 | 对于这个问题,建议结合抽数再加一步分析,进行一次后处理(如使用 QuantileFunction 中的 direct_exchange 项) 11 | ''' 12 | 13 | __all__ = [ 14 | 'PITY_6STAR', 15 | 'PITY_5STAR', 16 | 'P_5STAR_AVG', 17 | 'common_6star', 18 | 'single_up_6star_old', 19 | 'single_up_6star', 20 | 'dual_up_specific_6star_old', 21 | 'dual_up_specific_6star', 22 | 'new_first_dual_up_specific_6star', 23 | 'limited_up_6star', 24 | 'common_5star', 25 | 'single_up_specific_5star', 26 | 'dual_up_specific_5star', 27 | 'triple_up_specific_5star', 28 | 'limited_both_up_6star', 29 | ] 30 | 31 | # 设置6星概率递增表 32 | PITY_6STAR = np.zeros(100) 33 | PITY_6STAR[1:51] = 0.02 34 | PITY_6STAR[51:99] = np.arange(1, 49) * 0.02 + 0.02 35 | PITY_6STAR[99] = 1 36 | # 设置5星概率递增表(5星保底会被6星挤掉,所以需要做一点近似) 37 | PITY_5STAR = np.zeros(42, dtype=float) 38 | PITY_5STAR[:16] = 0.08 39 | PITY_5STAR[16:21] = np.arange(1, 6) * 0.02 + 0.08 40 | PITY_5STAR[21:41] = np.arange(1, 21) * 0.04 + 0.18 41 | PITY_5STAR[41] = 1 42 | # 设置5星综合概率,用于近似计算 43 | P_5STAR_AVG = 0.08948 44 | 45 | class AK_Limit_Model(CommonGachaModel): 46 | ''' 47 | 方舟限定池同时获取限定6星及陪跑6星模型 48 | 49 | - ``total_item_types`` 道具的总类别数 50 | - ``collect_item`` 收集道具的目标类别数 51 | ''' 52 | def __init__(self, pity_p, p, total_item_types=2, collect_item=None, e_error=1e-8, max_dist_len=1e5) -> None: 53 | super().__init__() 54 | if collect_item is None: 55 | collect_item = total_item_types 56 | self.layers.append(PityLayer(pity_p)) 57 | self.layers.append(BernoulliLayer(p, e_error, max_dist_len)) 58 | self.layers.append(CouponCollectorLayer(total_item_types, collect_item)) 59 | 60 | def __call__(self, multi_dist: bool = False, item_pity=0, *args: any, **kwds: any) -> Union[FiniteDist, list]: 61 | return super().__call__(1, multi_dist, item_pity, *args, **kwds) 62 | 63 | def _build_parameter_list(self, item_pity: int = 0, ) -> list: 64 | parameter_list = [ 65 | [[], {'item_pity': item_pity}], 66 | [[], {}], 67 | [[], {}] 68 | ] 69 | return parameter_list 70 | 71 | class HardTypePityDP(): 72 | ''' 73 | 类型硬保底DP(这里称原神的“平稳机制”为类型软保底,是一个类型的机制) 74 | 描述超过一定抽数没有获取某类道具时,下次再获取道具时必为某类道具之一,直至某类道具获得完全的模型 75 | 调用返回获得1个指定类型道具所需抽数分布 76 | TODO 稍加修改改为集齐k种道具所需的抽数分布 77 | ''' 78 | def __init__(self, item_pull_dist, type_pity_gap, item_types=2, up_rate=1, type_pull_shift=0) -> None: 79 | '''以获取此道具抽数分布、类型保底抽数、道具类别数量初始化''' 80 | self.item_pull_dist = item_pull_dist 81 | self.item_pity_pos = len(self.item_pull_dist) - 1 82 | self.type_pity_gap = type_pity_gap 83 | self.item_types = item_types 84 | self.up_rate = up_rate 85 | self.type_pull_shift = type_pull_shift # 用于处理 51 101 151 这类情况,shift=1 86 | 87 | # 输入检查 88 | if self.item_types <= 0: 89 | raise ValueError("item_types must above 0!") 90 | if self.item_pity_pos >= self.type_pity_gap: 91 | raise ValueError("Only support item_pity_pos < type_pity_pos") 92 | 93 | def __call__(self, item_pity=0, type_pity=0) -> np.ndarray: 94 | # 根据保底情况计算极限位置 95 | calc_pulls = len( 96 | self.item_pull_dist) - 1 + self.type_pity_gap * self.item_types - type_pity + self.type_pull_shift 97 | # 状态定义 98 | # 第0维为恰好在某一抽获得了道具,但一直没有目标类型 99 | # 第1维为是否获得目标类型 100 | M = np.zeros((calc_pulls + 1, 2), dtype=float) 101 | M[0, 0] = 1 102 | # 处理垫抽情况 103 | condition_dist = cut_dist(self.item_pull_dist, item_pity) 104 | # 开始DP 理论上用numpy矢量操作会快一点,但是这样不好写if就放弃了 105 | for i in range(1, calc_pulls + 1): 106 | # 枚举上次获得物品位置 107 | for j in range(max(0, i - self.item_pity_pos), i): 108 | # 有保底的情况下单独考虑 109 | use_dist = self.item_pull_dist 110 | if j == 0: 111 | use_dist = condition_dist 112 | if i - j >= len(use_dist): 113 | continue 114 | # 处理种类平稳情况,这里考虑的是倍数情况下 115 | if max(i - 1 + type_pity - self.type_pull_shift, 0) // self.type_pity_gap != max( 116 | j - 1 + type_pity - self.type_pull_shift, 0) // self.type_pity_gap: 117 | # 跨过平稳抽数的整倍数时触发了种类平稳,判断此时还没有抽到需求种类时转移的概率 118 | type_p = (max(i - 1 + type_pity - self.type_pull_shift, 0) // self.type_pity_gap) / self.item_types 119 | # print(j, i, type_p) 120 | else: 121 | # 没触发种类平稳 122 | type_p = self.up_rate / self.item_types 123 | M[i, 0] += M[j, 0] * (1 - type_p) * use_dist[i - j] 124 | M[i, 1] += M[j, 0] * type_p * use_dist[i - j] 125 | return M[:, 1] 126 | 127 | class AKHardPityModel(CommonGachaModel): 128 | ''' 129 | 针对通过统计发现的类型保底 130 | 131 | 该机制目前仅发现存在于标准寻访-双UP轮换池(不包含中坚寻访-双UP轮换池),尚未知该机制的累计次数是否会跨卡池继承, `详细信息参看一个资深的烧饼-视频 `_ 132 | ''' 133 | def __init__(self, no_type_pity_dist: FiniteDist, item_pull_dist, type_pity_gap, item_types=2, up_rate=1, 134 | type_pull_shift=0) -> None: 135 | super().__init__() 136 | self.DP_module = HardTypePityDP(item_pull_dist, type_pity_gap, item_types, up_rate, type_pull_shift) 137 | self.no_type_pity_dist = no_type_pity_dist 138 | 139 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity=0, type_pity=0) -> Union[ 140 | FiniteDist, list]: 141 | if not multi_dist: 142 | return FiniteDist(self.DP_module(item_pity, type_pity)) * self.no_type_pity_dist ** (item_num - 1) 143 | else: 144 | ans_list = [FiniteDist([1]), FiniteDist(self.DP_module(item_pity, type_pity))] 145 | for i in range(2, item_num + 1): 146 | ans_list.append(ans_list[i - 1] * self.no_type_pity_dist) 147 | return ans_list 148 | 149 | class AKDirectionalModel(CommonGachaModel): 150 | ''' 151 | 针对描述的寻访规则调整中提到的 `定向选调机制 `_ (在这里称为类型保底) 152 | 153 | - ``type_pity_gap`` 类型保底触发间隔抽数 154 | - ``item_types`` 道具类别个数,可以简单理解为UP了几个干员 155 | - ``up_rate`` 道具所占比例 156 | - ``type_pull_shift`` 保底类型偏移量,默认为0(无偏移) 157 | ''' 158 | def __init__(self, no_type_pity_dist: FiniteDist, item_pull_dist, type_pity_gap, item_types=2, up_rate=1, 159 | type_pull_shift=0) -> None: 160 | super().__init__() 161 | self.DP_module = HardTypePityDP(item_pull_dist, type_pity_gap, item_types, up_rate, type_pull_shift) 162 | self.no_type_pity_dist = no_type_pity_dist 163 | self.type_pity_gap = type_pity_gap 164 | self.item_pull_dist = item_pull_dist 165 | self.no_type_pity_dist = no_type_pity_dist 166 | self.type_pull_shift = type_pull_shift 167 | 168 | def _get_dist(self, item_num, item_pity, type_pity): 169 | if item_num == 1: 170 | return FiniteDist(self.DP_module(item_pity, type_pity)) 171 | # 第一个保底的概率分布 (没有归一化) 172 | c_dist = FiniteDist(self.DP_module(item_pity, type_pity)) 173 | c_dist[:self.type_pity_gap + self.type_pull_shift - type_pity + 1] = 0 174 | # 其他保底的概率分布 (没有归一化) 175 | f_dist = FiniteDist(self.DP_module(0, 0)) 176 | f_dist[:self.type_pity_gap + self.type_pull_shift + 1] = 0 177 | # 第一个不保底的概率分布 (没有归一化) 178 | first_lucky = FiniteDist( 179 | self.DP_module(item_pity, type_pity)[:self.type_pity_gap + self.type_pull_shift - type_pity + 1]) 180 | # 随后不保底的概率分布 (没有归一化) 181 | rest_lucky = FiniteDist(self.no_type_pity_dist[:self.type_pity_gap + self.type_pull_shift + 1]) 182 | 183 | ans = FiniteDist([0]) 184 | # 处理第一个就保底 185 | ans += (c_dist * self.no_type_pity_dist ** (item_num - 1)) 186 | # 处理其他情况 187 | for i in range(2, item_num + 1): 188 | ans += (first_lucky * rest_lucky ** (i - 2) * f_dist * self.no_type_pity_dist ** (item_num - i)) 189 | # 处理一直不保底的情况 190 | ans += first_lucky * rest_lucky ** (item_num - 1) 191 | return ans 192 | 193 | def __call__(self, item_num: int = 1, multi_dist: bool = False, item_pity=0, type_pity=0) -> Union[ 194 | FiniteDist, list]: 195 | if not multi_dist: 196 | return self._get_dist(item_num, item_pity, type_pity) 197 | else: 198 | ans_list = [FiniteDist([1])] 199 | for i in range(1, item_num + 1): 200 | ans_list.append(self._get_dist(i, item_pity, type_pity)) 201 | return ans_list 202 | 203 | # ★★★★★ 204 | # 5星公示概率为的8%,实际上综合概率为8.948% 这里按照综合概率近似计算 205 | # 注意,这里的5星并没有考虑类型保底和概率上升的情况,是非常简单的近似,具体类型保底机制详情见 https://www.bilibili.com/opus/772396224252739622 206 | # 获取普通5星 207 | common_5star = BernoulliGachaModel(P_5STAR_AVG) 208 | # 获取单UP5星 209 | single_up_specific_5star = BernoulliGachaModel(P_5STAR_AVG / 2) 210 | # 获取双UP5星中的特定5星 211 | dual_up_specific_5star = BernoulliGachaModel(P_5STAR_AVG / 2 / 2) 212 | # 获取三UP5星中的特定5星 213 | triple_up_specific_5star = BernoulliGachaModel(P_5STAR_AVG * 0.6 / 3) 214 | 215 | # ★★★★★★ 216 | # 注意有定向选调的情况只处理了第一个,接下来的没有处理,是有缺陷的,之后需要重写DP 217 | # 获取普通6星 218 | common_6star = PityModel(PITY_6STAR) 219 | # 获取单UP6星 无定向选调及有定向选调 定向选调见 https://www.bilibili.com/read/cv22596510/ 220 | single_up_6star_old = PityBernoulliModel(PITY_6STAR, 1 / 2) 221 | single_up_6star = AKDirectionalModel(single_up_6star_old(1), p2dist(PITY_6STAR), type_pity_gap=150, item_types=1, 222 | up_rate=0.5) 223 | # 获取双UP6星中的特定6星 无类型保底及有类型保底,此抽卡机制尚未完全验证 224 | dual_up_specific_6star_old = PityBernoulliModel(PITY_6STAR, 1 / 4) 225 | # 据统计 https://www.bilibili.com/video/BV1ib411f7YF/ 此卡池保底实际上从 202/402 等位置开始触发,因此此处 ``type_pull_shift`` 填写为 1 226 | dual_up_specific_6star = AKHardPityModel(dual_up_specific_6star_old(1), p2dist(PITY_6STAR), type_pity_gap=200, 227 | item_types=2, up_rate=0.5, type_pull_shift=1) 228 | # 据官方公示,部分新双UP卡池加入选调机制,https://www.bilibili.com/opus/1039209344221052937 此卡池保底实际上从 151/301 等位置开始触发,因此此处 ``type_pull_shift`` 填写为 0 229 | # 以下模型只适合计算获得第一个的情况 230 | new_first_dual_up_specific_6star = AKHardPityModel(dual_up_specific_6star_old(1), p2dist(PITY_6STAR), type_pity_gap=150, 231 | item_types=2, up_rate=0.5, type_pull_shift=0) 232 | 233 | # 获取限定UP6星中的限定6星 234 | limited_up_6star = PityBernoulliModel(PITY_6STAR, 0.35) 235 | # 同时获取限定池中两个UP6星(没有考虑井,不适用于有定向选调的卡池) 236 | limited_both_up_6star = AK_Limit_Model(PITY_6STAR, 0.7, total_item_types=2, collect_item=2) 237 | 238 | if __name__ == '__main__': 239 | pass 240 | -------------------------------------------------------------------------------- /GGanalysis/games/arknights/readme.md: -------------------------------------------------------------------------------- 1 | ## 模型注意 2 | 3 | 按照公示概率进行建模。但由于不熟悉具体机制,可能出错。 4 | 5 | 对于`300井`这类保底措施,由于有可能在第300抽时刚好抽到6星,等效于这一抽抽到了2个物品,不在框架内。故计算时不考虑这个因素,之后对计算出的分布进行后处理即可。 -------------------------------------------------------------------------------- /GGanalysis/games/arknights/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.arknights import PITY_6STAR, PITY_5STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在六星五星耦合情况下的概率,六星会重置五星保底,remov_pity 应设置为 True 5 | gacha_system = PriorityPitySystem([PITY_6STAR, PITY_5STAR], remove_pity=True) 6 | print('六星及五星平稳概率', gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/arknights_endfield/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/arknights_endfield/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | ''' 6 | 注意,本模块按明日方舟:终末地二测公示进行简单建模,非公测版本 7 | 武器池由于描述不够清晰,采用猜测机制 8 | ''' 9 | 10 | __all__ = [ 11 | 'PITY_6STAR', 12 | 'PITY_5STAR', 13 | 'PITY_W6STAR', 14 | 'PITY_W5STAR', 15 | 'common_6star', 16 | 'up_6star_character', 17 | 'up_6star_character_after_first', 18 | 'weapon_6star', 19 | 'up_6star_weapon', 20 | 'up_weapon_6star_after_first', 21 | 'common_5star', 22 | 'weapon_5star', 23 | ] 24 | class AKESinglePityModel(GachaModel): 25 | '''终末地二测硬保底模型''' 26 | def __init__(self, pity_p, up_p, up_pity_pull): 27 | super().__init__() 28 | self.pity_p = pity_p 29 | self.up_p = up_p 30 | self.up_pity_pull = up_pity_pull 31 | self.common_up_model = PityBernoulliModel(pity_p, up_p) 32 | 33 | def __call__(self, item_num: int=1, multi_dist: bool=False, item_pity=0, single_up_pity=0) -> Union[FiniteDist, list]: 34 | if item_num == 0: 35 | return FiniteDist([1]) 36 | # 如果 multi_dist 参数为真,返回抽取 [1, 抽取个数] 个道具的分布列表 37 | if multi_dist: 38 | return self._get_multi_dist(item_num, item_pity, single_up_pity) 39 | # 其他情况正常返回 40 | return self._get_dist(item_num, item_pity, single_up_pity) 41 | 42 | # 输入 [完整分布, 条件分布] 指定抽取个数,返回抽取 [1, 抽取个数] 个道具的分布列表 43 | def _get_multi_dist(self, item_num: int, item_pity, single_up_pity): 44 | # 仿造 CommonGachaModel 里的实现 45 | conditional_dist = self.common_up_model(1, item_pity=item_pity) 46 | first_dist = conditional_dist[:self.up_pity_pull-single_up_pity+1] 47 | first_dist[self.up_pity_pull-single_up_pity] = 1-sum(first_dist[:self.up_pity_pull-single_up_pity]) 48 | first_dist = FiniteDist(first_dist) # 定义抽第一个道具的分布 49 | ans_list = [FiniteDist([1]), first_dist] 50 | if item_num > 1: 51 | stander_dist = self.common_up_model(1) 52 | for i in range(1, item_num): 53 | ans_list.append(ans_list[i] * stander_dist) 54 | return ans_list 55 | 56 | # 返回单个分布 57 | def _get_dist(self, item_num: int, item_pity, single_up_pity): 58 | conditional_dist = self.common_up_model(1, item_pity=item_pity) 59 | first_dist = conditional_dist[:self.up_pity_pull-single_up_pity+1] 60 | first_dist[self.up_pity_pull-single_up_pity] = 1-sum(first_dist[:self.up_pity_pull-single_up_pity]) 61 | first_dist = FiniteDist(first_dist) # 定义抽第一个道具的分布 62 | if item_num == 1: 63 | return first_dist 64 | stander_dist = self.common_up_model(1) 65 | return first_dist * stander_dist ** (item_num-1) 66 | 67 | # 设置6星概率递增表 68 | PITY_6STAR = np.zeros(81) 69 | PITY_6STAR[1:65+1] = 0.008 70 | PITY_6STAR[66:81] = np.arange(1, 15+1) * 0.05 + 0.008 71 | PITY_6STAR[80] = 1 72 | # 设置5星概率递增表 73 | PITY_5STAR = np.zeros(11) 74 | PITY_5STAR[1:9+1] = 0.08 75 | PITY_5STAR[10] = 1 76 | # 设置武器池6星概率递增表 77 | PITY_W6STAR = np.zeros(41) 78 | PITY_W6STAR[1:39+1] = 0.04 79 | PITY_W6STAR[40] = 1 80 | # 设置武器池5星概率递增表 81 | PITY_W5STAR = np.zeros(11) 82 | PITY_W5STAR[1:9+1] = 0.15 83 | PITY_W5STAR[10] = 1 84 | 85 | # ★★★★★★ 86 | common_6star = PityModel(PITY_6STAR) 87 | up_6star_character = AKESinglePityModel(PITY_6STAR, 0.5, 120) 88 | up_6star_character_after_first = PityBernoulliModel(PITY_6STAR, 0.5) # 不考虑第一个 89 | weapon_6star = PityModel(PITY_W6STAR) 90 | up_6star_weapon = AKESinglePityModel(PITY_W6STAR, 0.25, 80) 91 | up_weapon_6star_after_first = PityBernoulliModel(PITY_W6STAR, 0.25) # 不考虑第一个 92 | # ★★★★★ 93 | # 五星公示基础概率为8%,更多信息还有待发掘,目前看来获取6星会重置5星保底 94 | common_5star = PityModel(PITY_5STAR) # 不考虑被6星重置的简单模型 95 | weapon_5star = PityModel(PITY_W5STAR) # 不考虑被6星重置的简单模型 96 | up_5star_character = PityBernoulliModel(PITY_5STAR, 0.5) # 不考虑被6星重置的简单模型 97 | 98 | if __name__ == '__main__': 99 | pass 100 | -------------------------------------------------------------------------------- /GGanalysis/games/arknights_endfield/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.arknights_endfield import PITY_6STAR, PITY_5STAR, PITY_W6STAR, PITY_W5STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在六星五星耦合情况下的概率,六星会重置五星保底,remov_pity 应设置为 True 5 | gacha_system = PriorityPitySystem([PITY_6STAR, PITY_5STAR], remove_pity=True) 6 | print('六星及五星平稳概率', gacha_system.get_stationary_p()) 7 | 8 | gacha_system = PriorityPitySystem([PITY_W6STAR, PITY_W5STAR], remove_pity=True) 9 | print('武器池六星及五星平稳概率', gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/azur_lane/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/azur_lane/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.basic_models import * 2 | 3 | __all__ = [ 4 | 'model_1_1_3', 5 | 'model_0_3_2', 6 | 'model_0_2_3', 7 | 'model_0_2_2', 8 | 'model_0_2_1', 9 | 'model_0_1_1', 10 | ] 11 | 12 | # 按卡池 彩 金 紫 道具数量进行模型命名 13 | model_1_1_3 = GeneralCouponCollectorModel([0.012, 0.02, 0.025/3, 0.025/3, 0.025/3], ['UR1', 'SSR1', 'SR1', 'SR2', 'SR3']) 14 | model_0_3_2 = GeneralCouponCollectorModel([0.02/3, 0.02/3, 0.02/3, 0.025/2, 0.025/2], ['SSR1', 'SSR2', 'SSR3', 'SR1', 'SR2']) 15 | model_0_2_3 = GeneralCouponCollectorModel([0.02/2, 0.02/2, 0.025/3, 0.025/3, 0.025/3], ['SSR1', 'SSR2', 'SR1', 'SR2', 'SR3']) 16 | model_0_2_2 = GeneralCouponCollectorModel([0.02/2, 0.02/2, 0.025/2, 0.025/2], ['SSR1', 'SSR2', 'SR1', 'SR2']) 17 | model_0_2_1 = GeneralCouponCollectorModel([0.02/2, 0.02/2, 0.025], ['SSR1', 'SSR2', 'SR1']) 18 | model_0_1_1 = GeneralCouponCollectorModel([0.02, 0.025], ['SSR1', 'SR1']) 19 | 20 | if __name__ == '__main__': 21 | # print(model_1_1_1().exp) 22 | import GGanalysis as gg 23 | from matplotlib import pyplot as plt 24 | # 对于 1_1_1 带200天井的情况,需要单独处理 25 | dist_0 = model_1_1_3(target_item=['UR1', 'SSR1']) # 未触发200天井时 26 | dist_1 = model_1_1_3(target_item=['SSR1']) # 触发200天井时 27 | cdf_0 = gg.dist2cdf(dist_0) 28 | cdf_1 = gg.dist2cdf(dist_1) 29 | cdf_0 = cdf_0[:min(len(cdf_0), len(cdf_1))] 30 | cdf_1 = cdf_1[:min(len(cdf_0), len(cdf_1))] 31 | cdf_0[200:] = cdf_1[200:] 32 | plt.plot(cdf_0) 33 | plt.show() 34 | # dist_ans = cdf2dist(cdf_0) 35 | 36 | -------------------------------------------------------------------------------- /GGanalysis/games/blue_archive/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * 2 | -------------------------------------------------------------------------------- /GGanalysis/games/blue_archive/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis as gg 2 | import GGanalysis.games.blue_archive as BA 3 | from GGanalysis.gacha_plot import QuantileFunction, DrawDistribution 4 | from GGanalysis.basic_models import cdf2dist 5 | from GGanalysis.plot_tools import * 6 | 7 | import matplotlib.pyplot as plt 8 | import matplotlib.cm as cm 9 | import numpy as np 10 | import os 11 | import time 12 | import copy 13 | 14 | def BA_character(x): 15 | return str(x)+'个' 16 | 17 | def plot_dual_collection_p(title="蔚蓝档案200抽内集齐两个UP学生概率", other_charactors=20, dpi=300, save_fig=False): 18 | model = BA.SimpleDualCollection(other_charactors=other_charactors) 19 | both_ratio, a_ratio, b_ratio, none_ratio = model.get_dist(calc_pull=200) 20 | # 使用fill_between画堆积图 21 | fig = plt.figure(figsize=(10, 6)) # 27寸4K显示器dpi=163 22 | ax = plt.gca() 23 | # 设置x,y范围,更美观 24 | ax.set_xlim(-5, 205) 25 | ax.set_ylim(0, 1.05) 26 | # 开启主次网格和显示及轴名称 27 | ax.grid(visible=True, which='major', linestyle='-', linewidth=1) 28 | ax.grid(visible=True, which='minor', linestyle='-', linewidth=0.5) 29 | ax.minorticks_on() 30 | ax.set_xlabel('抽数', weight='bold', size=12, color='black') 31 | ax.set_ylabel('累进概率', weight='bold', size=12, color='black') 32 | ax.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(1.0)) 33 | # 绘制右侧坐标轴 34 | ax2 = ax.twinx() 35 | ax2.set_ylim(0, 1.05) 36 | ax2.yaxis.set_major_formatter(mpl.ticker.PercentFormatter(1.0)) 37 | # ax2.set_ylabel('Y2 data', color='r') # 设置第二个y轴的标签 38 | # 绘制图像 39 | x = range(1, len(both_ratio)) 40 | plt.fill_between(x, 0, both_ratio[1:], label='集齐AB', color='C0', alpha=0.7, edgecolor='none', zorder=10) 41 | plt.fill_between(x, both_ratio[1:], both_ratio[1:]+a_ratio[1:], label='只获得A类', color='C0', alpha=0.5, edgecolor='none', zorder=10) 42 | plt.fill_between(x, both_ratio[1:]+a_ratio[1:], both_ratio[1:]+a_ratio[1:]+b_ratio[1:], label='只获得B类', color='C0', alpha=0.3, edgecolor='none', zorder=10) 43 | plt.plot(x, both_ratio[1:], color='C0', alpha=0.7, linewidth=2, zorder=10) 44 | plt.plot(x, both_ratio[1:]+a_ratio[1:], color='C0', alpha=0.5, linewidth=2, zorder=10) 45 | plt.plot(x, both_ratio[1:]+a_ratio[1:]+b_ratio[1:], color='C0', alpha=0.3, linewidth=2, zorder=10) 46 | # 添加来回切换策略 47 | swith_cdf = BA.pull_exchange_dp_1() 48 | plt.plot( 49 | range(1, len(swith_cdf)), swith_cdf[1:], color='gold', alpha=1, linewidth=2, zorder=10, linestyle='--', label='对比策略') 50 | # 添加描述文本 51 | ax.text( 52 | 30, 1.025, 53 | f"采用国服官方公示模型 A为本池UP角色 B为同期另一池角色,填充部分为堆积概率\n考虑常驻角色会越来越多,以除去UP角色外卡池内有{other_charactors}个角色的情况进行计算\n"+ 54 | f"对比策略指一直单抽,若在卡池A中抽到A,则换池抽B,无兑换下集齐概率\n@一棵平衡树 "+time.strftime('%Y-%m-%d',time.localtime(time.time())), 55 | weight='bold', 56 | size=12, 57 | color='#B0B0B0', 58 | path_effects=stroke_white, 59 | horizontalalignment='left', 60 | verticalalignment='top' 61 | ) 62 | # 图例和标题 63 | plt.legend(loc='upper left', prop={'weight': 'normal'}) 64 | plt.title(title, weight='bold', size=18) 65 | if save_fig: 66 | plt.savefig(os.path.join('figure', title+'.png'), dpi=dpi) 67 | else: 68 | plt.show() 69 | 70 | def get_dual_description( 71 | item_name='学生', 72 | cost_name='十连', 73 | text_head=None, 74 | mark_exp=None, 75 | direct_exchange=None, 76 | show_max_pull=None, 77 | is_finite=None, 78 | text_tail=None 79 | ): 80 | '''用于描述蔚蓝航线同时得到两个同时UP的角色''' 81 | description_text = '' 82 | # 开头附加文字 83 | if text_head is not None: 84 | description_text += text_head 85 | # 对道具期望值的描述 86 | if mark_exp is not None: 87 | if description_text != '': 88 | description_text += '\n' 89 | description_text += '集齐同时UP角色的期望抽数为'+format(mark_exp*10, '.2f')+'抽' 90 | # 对能否100%获取道具的描述 91 | description_text += '\n集齐同时UP角色最多需要400抽' 92 | # 末尾附加文字 93 | if text_tail is not None: 94 | if description_text != '': 95 | description_text += '\n' 96 | description_text += text_tail 97 | description_text = description_text.rstrip() 98 | return description_text 99 | 100 | # 神明文字返还 101 | def plot_refund(title="蔚蓝档案招募神名文字返还", dots=100, dpi=300, save_fig=False): 102 | ans_base = np.zeros(dots+1, dtype=float) 103 | for i in range(dots+1): 104 | ans_base[i] = BA.get_common_refund(rc3=i/dots) 105 | ans_high = np.zeros(dots+1, dtype=float) 106 | for i in range(dots+1): 107 | ans_high[i] = BA.get_common_refund(rc3=i/dots, has_up=True) 108 | # 使用fill_between画堆积图 109 | fig = plt.figure(figsize=(10, 6)) # 27寸4K显示器dpi=163 110 | ax = plt.gca() 111 | # 设置x,y范围,更美观 112 | ax.set_xlim(-0.01, 1.01) 113 | ax.set_ylim(0, 5.15) 114 | # 开启主次网格和显示及轴名称 115 | ax.grid(visible=True, which='major', linestyle='-', linewidth=1) 116 | ax.grid(visible=True, which='minor', linestyle='-', linewidth=0.5) 117 | ax.minorticks_on() 118 | ax.set_xlabel('常驻3星学生集齐比例', weight='bold', size=12, color='black') 119 | ax.set_ylabel('每抽获取神名文字期望数量', weight='bold', size=12, color='black') 120 | plt.xticks(np.arange(0, 1.1, 0.1)) 121 | ax.xaxis.set_major_formatter(mpl.ticker.PercentFormatter(1.0)) 122 | 123 | # 绘制图像 124 | plt.fill_between(np.linspace(0, 1, dots+1), 0, ans_base, color='C0', alpha=0.3, edgecolor='none', zorder=10) 125 | plt.plot(np.linspace(0, 1, dots+1), ans_base, color='C0', alpha=1, linewidth=2, label='初始无UP', zorder=10) 126 | 127 | plt.plot(np.linspace(0, 1, dots+1), ans_high, color='C0', alpha=1, linestyle=':', linewidth=2, label='初始有UP', zorder=10) 128 | # 添加描述文本 129 | ax.text( 130 | 30, 1.025, 131 | f"采用国服官方公示模型 \n不考虑每200抽兑换\n"+ 132 | f"@一棵平衡树 "+time.strftime('%Y-%m-%d',time.localtime(time.time())), 133 | weight='bold', 134 | size=12, 135 | color='#B0B0B0', 136 | path_effects=stroke_white, 137 | horizontalalignment='left', 138 | verticalalignment='top' 139 | ) 140 | # 图例和标题 141 | plt.legend(loc='upper left', prop={'weight': 'normal'}) 142 | plt.title(title, weight='bold', size=18) 143 | if save_fig: 144 | plt.savefig(os.path.join('figure', title+'.png'), dpi=dpi) 145 | else: 146 | plt.show() 147 | 148 | if __name__ == '__main__': 149 | # 传统方法分析UP学生抽取,3星升到最高级需要220学生神名文字,重复返还100,所以最多需要抽4个 150 | BA_up3dist = [gg.FiniteDist()] 151 | for i in range(1, 4+1): 152 | BA_up3dist.append(BA.up_3star(i)) 153 | BA_fig = QuantileFunction( 154 | BA_up3dist, 155 | title='蔚蓝档案获取UP三星学生概率(考虑200井)', 156 | item_name='特定UP三星角色', 157 | text_head=f'招募内特定UP三星学生概率为{BA.P_3UP:.1%}\n考虑每200抽的招募点数兑换\n不考虑神名文字直接兑换学生神名文字', 158 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 159 | max_pull=850, 160 | mark_func=BA_character, 161 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 4+1)), 162 | direct_exchange=200, 163 | y2x_base=2, 164 | 165 | plot_direct_exchange=True, 166 | is_finite=False) 167 | BA_fig.show_figure(dpi=300, savefig=True) 168 | 169 | # 传统方法分析UP学生抽取,2星升到最高级需要300学生神名文字,重复返还20,所以最多需要抽15个 170 | BA_up2dist = [gg.FiniteDist()] 171 | for i in range(1, 15+1): 172 | BA_up2dist.append(BA.up_2star(i)) 173 | BA_fig = QuantileFunction( 174 | BA_up2dist, 175 | title='蔚蓝档案获取UP两星学生概率', 176 | item_name='特定UP两星角色', 177 | text_head=f'招募内特定UP两星学生概率为{BA.P_2UP:.1%}\n不考虑神名文字直接兑换学生神名文字', 178 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 179 | max_pull=950, 180 | mark_func=BA_character, 181 | line_colors=cm.GnBu(np.linspace(0.4, 0.9, 15+1)), 182 | y2x_base=2, 183 | 184 | plot_direct_exchange=True, 185 | is_finite=False) 186 | BA_fig.show_figure(dpi=300, savefig=True) 187 | 188 | # 分析无保底时同时集齐UP的两个学生的概率(考虑换池抽) 189 | plot_dual_collection_p(dpi=300, save_fig=True) 190 | 191 | # 分析同时集齐UP的两个学生的抽数分布(每次十连,抽到了就换池抽) 192 | # 集齐两个UP角色 193 | temp_dist = cdf2dist(BA.pull_exchange_dp_10()) 194 | BA_fig = DrawDistribution( 195 | dist_data=temp_dist, 196 | title='蔚蓝档案集齐同时UP的两个学生', 197 | quantile_pos=[0.05, 0.1, 0.25, 0.5, 0.75, 0.8, 0.9, 0.95, 1], 198 | max_pull=41, 199 | text_head=f'采用官方公示模型\n视常驻有{BA.STANDER_3STAR}个角色,计入每{BA.EXCHANGE_PULL}抽兑换\n按每次十连抽,抽到对应学生则换池\n集齐即停止计算(即没抽到井集齐了不补到井)', 200 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 201 | description_pos=0, 202 | item_name='3星学生', 203 | cost_name='十连', 204 | is_finite=True, 205 | description_func=get_dual_description, 206 | ) 207 | BA_fig.show_two_graph(dpi=300, savefig=True) 208 | 209 | # 计算神名文字返还期望 210 | plot_refund(save_fig=True) 211 | 212 | 213 | -------------------------------------------------------------------------------- /GGanalysis/games/blue_archive/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.basic_models import cdf2dist 2 | from GGanalysis.coupon_collection import GeneralCouponCollection 3 | from GGanalysis.basic_models import * 4 | 5 | __all__ = [ 6 | 'P_3', 7 | 'P_2', 8 | 'P_1', 9 | 'P_2_TENPULL', 10 | 'P_1_TENPULL', 11 | 'P_3UP', 12 | 'P_2UP', 13 | 'up_3star', 14 | 'up_2star', 15 | 'SimpleDualCollection', 16 | 'pull_exchange_dp_1', 17 | 'pull_exchange_dp_10', 18 | 'get_common_refund', 19 | 'STANDER_3STAR', 20 | 'EXCHANGE_PULL', 21 | ] 22 | 23 | P_3 = 0.03 24 | P_2 = 0.185 25 | P_1 = 0.785 26 | 27 | """ 28 | 十连抽时2星综合概率 29 | = E(十连抽到的2星数量) / 10 30 | = (E(前9抽抽到的2星数量) + P(第10抽抽到2星)) / 10 31 | = (9 * P(第1抽抽到2星) + P(第10抽抽到2星 | 前9抽未抽到2星及以上) * P(前9抽未抽到2星及以上) + P(第10抽抽到2星 | 前9抽已抽到2星及以上) * P(前9抽已抽到2星及以上)) / 10 32 | = (9 * 18.50 % + 97 % * 78.50 % ^ 9 + 18.50 % * (1 - 78.50 % ^ 9)) / 10 33 | = 198539059901039401398249/1024000000000000000000000 34 | = 0.19388580068460878 35 | """ 36 | P_2_TENPULL = (9 * P_2 + (1 - P_3) * P_1 ** 9 + P_2 * (1 - P_1 ** 9)) / 10 37 | P_1_TENPULL = 1 - 0.03 - P_2_TENPULL 38 | 39 | P_3UP = 0.007 40 | P_2UP = 0.03 41 | 42 | STANDER_3STAR = 20 43 | EXCHANGE_PULL = 200 44 | 45 | up_3star = BernoulliGachaModel(P_3UP) 46 | up_2star = BernoulliGachaModel(P_2UP) 47 | 48 | class SimpleDualCollection(): 49 | '''考虑一直抽一个池,同时集齐两个卡池UP的概率''' 50 | def __init__(self, base_p=P_3, up_p=P_3UP, other_charactors=STANDER_3STAR) -> None: 51 | self.model = GeneralCouponCollection([up_p, (base_p-up_p)/other_charactors], ['A_UP', 'B_UP']) 52 | 53 | def get_dist(self, calc_pull=EXCHANGE_PULL): 54 | M = self.model.collection_dp(calc_pull) 55 | end_state = self.model.encode_state_number(['A_UP', 'B_UP']) 56 | a_state = self.model.encode_state_number(['A_UP']) 57 | b_state = self.model.encode_state_number(['B_UP']) 58 | none_state = self.model.encode_state_number([]) 59 | both_ratio = M[end_state, :] 60 | a_ratio = M[a_state, :] 61 | b_ratio = M[b_state, :] 62 | none_ratio = M[none_state, :] 63 | return both_ratio, a_ratio, b_ratio, none_ratio 64 | 65 | def pull_exchange_dp_1(base_p=P_3, up_p=P_3UP, other_charactors=STANDER_3STAR, exchange_pos=EXCHANGE_PULL): 66 | '''在重复角色获得神名文字无价值情情况下用最低抽数集齐的策略,即会切换卡池抽,每次单抽''' 67 | # 即获得了A后就换到B池继续抽的情况 68 | import numpy as np 69 | # A为默认先抽角色 B默认为后抽角色 70 | # 设置状态有三:两者都集齐了的状态11,获得了A的状态01,获得了B的状态10,两者都没有的00 71 | M = np.zeros((exchange_pos+1, 4), dtype=float) 72 | M[0, 0] = 1 73 | # 根据状态转移进行DP 74 | for pull in range(1, exchange_pos+1): 75 | M[pull, 0] += M[pull-1, 0] * (1-up_p-(base_p-up_p)/other_charactors) 76 | 77 | M[pull, 1] += M[pull-1, 0] * up_p 78 | M[pull, 1] += M[pull-1, 1] * (1-up_p) 79 | 80 | M[pull, 2] += M[pull-1, 0] * (base_p-up_p)/other_charactors 81 | M[pull, 2] += M[pull-1, 2] * (1-up_p) 82 | 83 | M[pull, 3] += M[pull-1, 2] * up_p 84 | M[pull, 3] += M[pull-1, 1] * up_p 85 | M[pull, 3] += M[pull-1, 3] 86 | 87 | # 以下是用于验证的不更换卡池的策略 88 | # M[pull, 0] += M[pull-1, 0] * (1-up_p-(base_p-up_p)/other_charactors) 89 | 90 | # M[pull, 1] += M[pull-1, 0] * up_p 91 | # M[pull, 1] += M[pull-1, 1] * (1-(base_p-up_p)/other_charactors) 92 | 93 | # M[pull, 2] += M[pull-1, 0] * (base_p-up_p)/other_charactors 94 | # M[pull, 2] += M[pull-1, 2] * (1-up_p) 95 | 96 | # M[pull, 3] += M[pull-1, 2] * up_p 97 | # M[pull, 3] += M[pull-1, 1] * (base_p-up_p)/other_charactors 98 | # M[pull, 3] += M[pull-1, 3] 99 | 100 | return M[:, 3] 101 | 102 | def pull_exchange_dp_10(base_p=P_3, up_p=P_3UP, other_charactors=STANDER_3STAR, exchange_pos=EXCHANGE_PULL): 103 | '''在重复角色获得神名文字无价值情情况下用最低抽数集齐的策略,即会切换卡池抽,每次十连抽''' 104 | a, b = up_p, (base_p-up_p)/other_charactors 105 | # 设置每种状态下的转移矩阵,开始位置为先抽A卡池 106 | M_A = np.array([[1-a-b, 0, 0, 0], 107 | [a, 1-b, 0, 0], 108 | [b, 0, 1-a, 0], 109 | [0, b, a, 1]]) 110 | M_B = np.array([[1-a-b, 0, 0, 0], 111 | [b, 1-a, 0, 0], 112 | [a, 0, 1-b, 0], 113 | [0, a, b, 1]]) 114 | # 十连自乘10次 115 | M_A_10 = np.linalg.matrix_power(M_A, 10) 116 | M_B_10 = np.linalg.matrix_power(M_B, 10) 117 | # 得到策略组合后的矩阵 118 | M = np.hstack((M_A_10[:, [0]], M_B_10[:, [1]], M_A_10[:, [2]], M_B_10[:, [3]])) 119 | 120 | # 递推得到结果 121 | k = int(exchange_pos/10) * 2 # 十连次数 122 | ans = np.zeros((k+1, 4), dtype=float) 123 | x = np.array([1, 0, 0, 0]) 124 | # 计算所有情况 125 | for i in range(0, k+1): 126 | ans[i, :] = x 127 | x = np.matmul(M, x) 128 | 129 | # 对井的情况进行处理 130 | ans[20:, 3] += ans[20:, 1] 131 | ans[20:, 3] += ans[20:, 2] 132 | ans[40:, 3] = 1 133 | 134 | return ans[:, 3] 135 | 136 | def no_exchange_dp_10(base_p=P_3, up_p=P_3UP, other_charactors=STANDER_3STAR, exchange_pos=EXCHANGE_PULL): 137 | '''上面的函数的不切换抽到井版本''' 138 | a, b = up_p, (base_p-up_p)/other_charactors 139 | # 设置每种状态下的转移矩阵,开始位置为先抽A卡池 140 | M_A = np.array([[1-a-b, 0, 0, 0], 141 | [a, 1-b, 0, 0], 142 | [b, 0, 1-a, 0], 143 | [0, b, a, 1]]) 144 | # 十连自乘10次 145 | M_A_10 = np.linalg.matrix_power(M_A, 10) 146 | # 得到策略组合后的矩阵 147 | M = M_A_10 148 | 149 | # 递推得到结果 150 | k = int(exchange_pos/10) * 2 # 十连次数 151 | ans = np.zeros((k+1, 4), dtype=float) 152 | x = np.array([1, 0, 0, 0]) 153 | # 计算所有情况 154 | for i in range(0, k+1): 155 | ans[i, :] = x 156 | x = np.matmul(M, x) 157 | 158 | # 对井的情况进行处理 159 | ans[20:, 3] += ans[20:, 1] 160 | ans[20:, 3] += ans[20:, 2] 161 | ans[40:, 3] = 1 162 | 163 | return ans[:, 3] 164 | 165 | def get_common_refund(rc3=0.5, rc2=1, rc1=1,has_up=False): 166 | '''按每个卡池抽200计算的平均每抽神名文字返还''' 167 | e_up = 0 # 超出1个数量的期望 168 | if has_up: 169 | e_up = EXCHANGE_PULL * P_3UP + 1 # 实质上就是线性上移 170 | else: 171 | e_up = EXCHANGE_PULL * P_3UP # 井和超出1个部分减一相互抵消 172 | rc_up = e_up / EXCHANGE_PULL 173 | return ((P_3-P_3UP) * rc3 + rc_up) * 50 + P_2_TENPULL * 10 * rc2 + P_1_TENPULL * 1 * rc1 174 | 175 | 176 | if __name__ == '__main__': 177 | import copy 178 | from GGanalysis.distribution_1d import * 179 | 180 | # 平稳时来自保底的比例 181 | print("平稳时来自保底的比例为", 1/(EXCHANGE_PULL*0.03+1), "UP来自保底的比例为", 1/(EXCHANGE_PULL*0.007+1)) 182 | 183 | # 十连的实际综合概率 184 | print(f"十连时三星概率{P_3} 两星概率{P_2_TENPULL} 一星概率{P_1},两星概率相比不十连提升{100*(P_2_TENPULL/P_2-1)}%") 185 | 186 | # 计算单抽仅获取UP抽数期望(拥有即算,在200抽抽到时虽然可以多井一个但也算保证获取一个UP 187 | model = SimpleDualCollection(other_charactors=STANDER_3STAR) 188 | both_ratio, a_ratio, b_ratio, none_ratio = model.get_dist(calc_pull=EXCHANGE_PULL*2) 189 | temp_dist = both_ratio+a_ratio 190 | temp_dist = temp_dist[:201] 191 | temp_dist[200] = 1 192 | temp_dist = cdf2dist(temp_dist) 193 | print("含井单抽仅获取UP角色的抽数期望", temp_dist.exp) 194 | # 含井单抽仅获取UP角色的抽数期望 107.80200663752993 195 | 196 | # 计算十连抽仅获取UP抽数期望(拥有即算,在200抽抽到时虽然可以多井一个但也算保证获取一个UP 197 | temp_dist = both_ratio+a_ratio 198 | temp_dist = temp_dist[:201] 199 | temp_dist[200] = 1 200 | temp_dist = cdf2dist(temp_dist) 201 | temp_dist = dist_squeeze(temp_dist, 10) 202 | print("含井十连仅获取UP角色的抽数期望", temp_dist.exp*10) 203 | # 含井十连仅获取UP角色的抽数期望 111.24149841754267 204 | 205 | 206 | # 计算获取同期两个UP,采用抽1井1方法的期望(按照每次十连,出了对应学生就换池的方法进行) 207 | temp_dist = cdf2dist(pull_exchange_dp_10()) 208 | print("一直十连抽,抽1井1获得角色即换池策略下获得同时UP的两类角色的抽数期望", temp_dist.exp*10) 209 | # 一直十连抽,抽1井1获得角色即换池策略下获得同时UP的两类角色的抽数期望 185.85079879472937 210 | 211 | # 计算获取同期两个UP,采用抽1井1方法的期望(按照每次十连,但是不换池的方法) 212 | temp_dist = cdf2dist(no_exchange_dp_10()) 213 | print("不换池情况的期望", temp_dist.exp*10) 214 | 215 | # 计算含/不含井的神名文字返还 216 | 217 | # 粗估UP池内抽满一个角色的期望(含兑换,按无限平均估算,按5:1兑换并认为其他角色都已拥有),3星升级5星需要220神名文字 218 | # 这个计算严重失真,因为不是这么有机会可以井到 219 | E_up_pull = (P_3UP + 1/200) * 100 # + get_common_refund(rc3=1, has_up=1) * 0.2 220 | left = 220 # - 107.80200663752993 * get_common_refund(rc3=0.5, has_up=0) * 0.2 221 | print("拥有角色后,每抽平均获得角色神名文字:", E_up_pull) 222 | print("抽满一个角色的估计抽数是:", 107.80200663752993 + left / E_up_pull) 223 | # 抽满一个角色的估计抽数是: 291.13533997086324 224 | 225 | -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * 2 | from .artifact_model import * -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/artifact_data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 原神圣遗物数值表 3 | ''' 4 | ARTIFACT_TYPES = ['flower', 'plume', 'sands', 'goblet', 'circlet'] 5 | 6 | ARTIFACT_NAME = { 7 | 'flower': '生之花', 8 | 'plume': '死之羽', 9 | 'sands': '时之沙', 10 | 'goblet': '空之杯', 11 | 'circlet': '理之冠' 12 | } 13 | 14 | # 类型别名对照表 15 | STAT_NAME = { 16 | 'hp': '数值生命值', 17 | 'atk': '数值攻击力', 18 | 'def': '数值防御力', 19 | 'hpp': '百分比生命值', 20 | 'atkp': '百分比攻击力', 21 | 'defp': '百分比防御力', 22 | 'pyroDB': '火元素伤害加成', 23 | 'hydroDB': '水元素伤害加成', 24 | 'anemoDB': '风元素伤害加成', 25 | 'electroDB': '雷元素伤害加成', 26 | 'dendroDB': '草元素伤害加成', 27 | 'cryoDB': '冰元素伤害加成', 28 | 'geoDB': '岩元素伤害加成', 29 | 'physicalDB': '物理伤害加成', 30 | 'em': '元素精通', 31 | 'er': '元素充能效率', 32 | 'cr': '暴击率', 33 | 'cd': '暴击伤害', 34 | 'hb': '治疗加成', 35 | } 36 | 37 | # 所有主词条掉落权重表 38 | # 掉落权重取自 tesiacoil 的整理 39 | # 主词条 https://wiki.biligame.com/ys/掉落系统学/常用数据#主词条 40 | W_MAIN_STAT = { 41 | 'flower': {'hp': 1000}, 42 | 'plume': {'atk': 1000}, 43 | 'sands': { 44 | 'hpp': 1334, 45 | 'atkp': 1333, 46 | 'defp': 1333, 47 | 'em': 500, 48 | 'er': 500, 49 | }, 50 | 'goblet': { 51 | 'hpp': 770, 52 | 'atkp': 770, 53 | 'defp': 760, 54 | 'pyroDB': 200, 55 | 'electroDB': 200, 56 | 'cryoDB': 200, 57 | 'hydroDB': 200, 58 | 'dendroDB': 200, 59 | 'anemoDB': 200, 60 | 'geoDB': 200, 61 | 'physicalDB': 200, 62 | 'em': 100, 63 | }, 64 | 'circlet': { 65 | 'hpp': 1100, 66 | 'atkp': 1100, 67 | 'defp': 1100, 68 | 'cr': 500, 69 | 'cd': 500, 70 | 'hb': 500, 71 | 'em': 200, 72 | } 73 | } 74 | 75 | # 所有副词条权重表 76 | # 掉落权重取自 tesiacoil 的整理 77 | # 副词条 https://wiki.biligame.com/ys/掉落系统学/常用数据#副词条 78 | W_SUB_STAT = { 79 | 'hp': 150, 80 | 'atk': 150, 81 | 'def': 150, 82 | 'hpp': 100, 83 | 'atkp': 100, 84 | 'defp': 100, 85 | 'em': 100, 86 | 'er': 100, 87 | 'cr': 75, 88 | 'cd': 75, 89 | } 90 | 91 | # 五星圣遗物不同来源多词条占比 https://genshin-impact.fandom.com/wiki/Loot_System/Artifact_Drop_Distribution 92 | P_DROP_STATE = { 93 | 'domains_drop': 0.2, 94 | 'normal_boos_drop': 0.34, 95 | 'weekly_boss_drop': 0.34, 96 | 'converted_by_alchemy_table': 0.34, 97 | } 98 | 99 | # 五星圣遗物主词条满级时属性 https://genshin-impact.fandom.com/wiki/Artifact/Stats 100 | MAIN_STAT_MAX = { 101 | 'hp': 4780, 102 | 'atk': 311, 103 | 'hpp': 0.466, 104 | 'atkp': 0.466, 105 | 'defp': 0.583, 106 | 'em': 186.5, 107 | 'er': 0.518, 108 | 'pyroDB': 0.466, 109 | 'hydroDB': 0.466, 110 | 'anemoDB': 0.466, 111 | 'electroDB': 0.466, 112 | 'dendroDB': 0.466, 113 | 'cryoDB': 0.466, 114 | 'geoDB': 0.466, 115 | 'physicalDB': 0.583, 116 | 'cr': 0.311, 117 | 'cd': 0.622, 118 | 'hb': 0.359, 119 | } 120 | 121 | # 五星副词条单次升级最大值 122 | # 全部的精确值见 https://nga.178.com/read.php?tid=31774495 123 | SUB_STAT_MAX = { 124 | 'hp': 298.75, 125 | 'atk': 19.45, 126 | 'def': 23.14, 127 | 'hpp': 0.0583, 128 | 'atkp': 0.0583, 129 | 'defp': 0.0729, 130 | 'em': 23.31 , 131 | 'er': 0.0648, 132 | 'cr': 0.0389, 133 | 'cd': 0.0777, 134 | } 135 | 136 | # 圣遗物强化经验翻倍概率 https://wiki.biligame.com/ys/掉落系统学/常用数据#主词条 137 | P_EXP_MULTI = { 138 | '1': 0.9, 139 | '2': 0.09, 140 | '5': 0.01, 141 | } 142 | 143 | # 默认打分权重,这个值可以是 [0,1] 的浮点数 144 | DEFAULT_STAT_SCORE = { 145 | 'hp': 0, 146 | 'atk': 0, 147 | 'def': 0, 148 | 'hpp': 0, 149 | 'atkp': 1, 150 | 'defp': 0, 151 | 'em': 0, 152 | 'er': 0, 153 | 'cr': 1, 154 | 'cd': 1, 155 | } 156 | 157 | # 默认主属性选择 158 | DEFAULT_MAIN_STAT = { 159 | 'flower': 'hp', 160 | 'plume': 'atk', 161 | 'sands': 'atkp', 162 | 'goblet': 'pyroDB', 163 | 'circlet': 'cr', 164 | } 165 | 166 | # 默认颜色 167 | DEFAULT_STAT_COLOR = { 168 | 'hp': '#4e8046', 169 | 'hpp': '#65a65b', 170 | 'atk': '#8c4646', 171 | 'atkp': '#b35959', 172 | 'def': '#8c8c3f', 173 | 'defp': '#b2b350', 174 | 'pyroDB': '#d96857', 175 | 'hydroDB': '#6c77d9', 176 | 'anemoDB': '#4dbf99', 177 | 'electroDB': '#c566cc', 178 | 'dendroDB': '#59b364', 179 | 'cryoDB': '#6cd5d9', 180 | 'geoDB': '#bfb560', 181 | 'physicalDB': '#7a8c99', 182 | 'em': '#a15ba6', 183 | 'er': '#665ba6', 184 | 'cr': '#f29224', 185 | 'cd': '#f24124', 186 | 'hb': '#79a63a', 187 | } -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/artifact_example.py: -------------------------------------------------------------------------------- 1 | import GGanalysis as gg 2 | import GGanalysis.games.genshin_impact as GI 3 | from GGanalysis.ScoredItem import combine_items 4 | 5 | # 注意,以下定义的分数指每次副词条强化为最高属性为10,其他情况依次为9、8、7 6 | 7 | # 定义圣遗物生之花 8 | flower = GI.GenshinArtifact(type='flower') 9 | print('在获取100件生之花后,获取的最高分数分布') 10 | print(flower.repeat(100, p=1).score_dist.dist) 11 | print('在祝圣秘境获取100件圣遗物后,其中特定套装的生之花的最高分数的分布') 12 | print(flower.repeat(100, p=1/10).score_dist.dist) 13 | # 使用「祝圣之霜」自定义双暴副词条生之花 14 | defined_flower = GI.GenshinDefinedArtifact('flower', 'hp', ['cr', 'cd']) 15 | print('使用「祝圣之霜」自定义一次双暴副词条生之花,得到分数的分布') 16 | print(defined_flower.score_dist.dist) 17 | 18 | # 定义以默认权重计算的圣遗物套装 19 | default_weight_artifact = GI.GenshinArtifactSet() 20 | print('在祝圣秘境获取100件圣遗物后,其中特定套装组5件套能获得的最高分数的分布') 21 | print(combine_items(default_weight_artifact.repeat(100)).score_dist.dist) 22 | print('在祝圣秘境获取100件圣遗物,获取散件1500件后,其中特定套装和散件组4+1能获得的最高分数的分布') 23 | print(default_weight_artifact.get_4piece_under_condition(n=100, base_n=1500).score_dist.dist) 24 | 25 | # 自定义属性,原神词条别名说明见 games/genshin_impact/artifact_data.py 26 | # 自定义主词条选择 27 | MAIN_STAT = { 28 | 'flower': 'hp', 29 | 'plume': 'atk', 30 | 'sands': 'atkp', 31 | 'goblet': 'pyroDB', 32 | 'circlet': 'cd', 33 | } 34 | # 自定义副词条权重 35 | STAT_SCORE = { 36 | 'atkp': 0.5, 37 | 'em': 0.6, 38 | 'er': 0.3, 39 | 'cr': 1, 40 | 'cd': 1, 41 | } 42 | custom_weight_artifact = GI.GenshinArtifactSet(main_stat=MAIN_STAT, stats_score=STAT_SCORE) 43 | print('自定义条件下,在祝圣秘境获取100件圣遗物,获取散件1500件后,其中特定套装和散件组4+1能获得的最高分数的分布') 44 | print(default_weight_artifact.get_4piece_under_condition(n=100, base_n=1500).score_dist.dist) 45 | -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.genshin_impact as GI 2 | from GGanalysis.gacha_plot import QuantileFunction 3 | import matplotlib.cm as cm 4 | import numpy as np 5 | import time 6 | 7 | # 绘制原神抽卡概率分位函数图 8 | 9 | # 定义获取物品个数的别名 10 | def gi_character(x): 11 | return str(x-1)+'命' 12 | def gi_weapon(x): 13 | return str(x)+'精' 14 | 15 | # 原神UP五星角色 16 | GI_fig = QuantileFunction( 17 | GI.up_5star_character(item_num=7, multi_dist=True), 18 | title='原神UP五星角色抽取概率', 19 | item_name='UP五星角色', 20 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中', 21 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 22 | max_pull=1300, 23 | mark_func=gi_character, 24 | is_finite=True) 25 | GI_fig.show_figure(dpi=300, savefig=True) 26 | 27 | # 原神特定UP四星角色 28 | GI_fig = QuantileFunction( 29 | GI.up_4star_specific_character(item_num=7, multi_dist=True), 30 | title='原神特定UP四星角色抽取概率', 31 | item_name='特定UP四星', 32 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中\n绘图曲线忽略五星与四星耦合情况', 33 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 34 | max_pull=600, 35 | mark_func=gi_character, 36 | line_colors=cm.Purples(np.linspace(0.5, 0.9, 7+1)), 37 | is_finite=False) 38 | GI_fig.show_figure(dpi=300, savefig=True) 39 | 40 | # 原神定轨UP五星武器 41 | GI_fig = QuantileFunction( 42 | GI.up_5star_ep_weapon(item_num=5, multi_dist=True), 43 | title='原神5.0后定轨UP五星武器抽取概率', 44 | item_name='定轨五星武器', 45 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中', 46 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 47 | max_pull=800, 48 | mark_func=gi_weapon, 49 | line_colors=cm.Reds(np.linspace(0.5, 0.9, 5+1)), 50 | is_finite=True) 51 | GI_fig.show_figure(dpi=300, savefig=True) 52 | 53 | GI_fig = QuantileFunction( 54 | GI.classic_up_5star_ep_weapon(item_num=5, multi_dist=True), 55 | title='旧版本-原神5.0前定轨UP五星武器抽取概率', 56 | item_name='定轨五星武器', 57 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中', 58 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 59 | max_pull=1200, 60 | mark_func=gi_weapon, 61 | line_colors=cm.Reds(np.linspace(0.5, 0.9, 5+1)), 62 | is_finite=True) 63 | GI_fig.show_figure(dpi=300, savefig=True) 64 | 65 | # 原神不定轨UP五星武器 66 | GI_fig = QuantileFunction( 67 | GI.classic_up_5star_specific_weapon(item_num=5, multi_dist=True), 68 | title='原神不定轨特定UP五星武器抽取概率', 69 | item_name='特定五星武器', 70 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中', 71 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 72 | max_pull=1600, 73 | y2x_base=1.6, 74 | mark_func=gi_weapon, 75 | line_colors=(cm.Greys(np.linspace(0.5, 0.9, 5+1))+cm.Reds(np.linspace(0.5, 0.9, 5+1)))/2, 76 | is_finite=False) 77 | GI_fig.show_figure(dpi=300, savefig=True) 78 | 79 | # 原神特定UP四星武器 80 | GI_fig = QuantileFunction( 81 | GI.up_4star_specific_weapon(item_num=5, multi_dist=True), 82 | title='原神特定UP四星武器抽取概率', 83 | item_name='特定UP四星', 84 | text_head='采用www.bilibili.com/read/cv10468091模型\n本算例中UP物品均不在常驻祈愿中\n绘图曲线忽略五星与四星耦合情况', 85 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 86 | max_pull=600, 87 | mark_func=gi_weapon, 88 | line_colors=cm.Oranges(np.linspace(0.5, 0.9, 5+1)), 89 | is_finite=False) 90 | GI_fig.show_figure(dpi=300, savefig=True) -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/get_both_EP_weapon.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from GGanalysis import FiniteDist 3 | 4 | def calc_EP_weapon_classic(a, b): 5 | ''' 6 | 返回原神5.0版本前2.0版本后同卡池抽A限定五星武器a个和B限定五星武器b个所需五星个数分布 7 | 8 | 计算5.0前2.0后命定值为2的情况下,恰好花费k个五星后恰好抽到a个A限定UP五星和b个B限定UP五星的概率, 9 | 抽取时采用最优策略,若a=b,则定轨当前离要求数量较远的一个限定UP五星, 10 | 若a≠b,则同样定轨当前离要求数量较远的限定UP五星,当离要求数量一致时,转化为a=b问题, 11 | 若a=b,至多需要获取3*a个五星;若a≠b,至多需要获取3*max(a,b)个五星。 12 | ''' 13 | # S 第0维表示获得A的数量 第1维表示获得B的数量 第2维表示获得常驻的数量 14 | S = np.zeros((60, 60, 60)) 15 | S[(0, 0, 0)] = 1 16 | N = np.zeros((60, 60, 60)) 17 | # 记录最后到达每个状态概率 18 | ans_dist = np.zeros((60, 60, 60)) 19 | num_dist = np.zeros(200) 20 | # 控制状态更新,注意同样的状态可以在不同轮进行反复更新 21 | 22 | E = 0 # 期望值 23 | # 枚举定轨轮数 24 | for ep in range(3*max(a, b)+1): # 此处+1是为了扫尾 25 | for i in range(2*max(a, b)+2): # 获得A的数量 +2 是为了先去A后再去B使得A多1 26 | for j in range(2*max(a, b)+1): # 获得B的数量 27 | for k in range(a+b+1): # 获得常驻的数量 28 | # 若已经满足条件,则不继续 29 | if i>=a and j >=b: 30 | # if i+j+k >= 21 and S[i,j,k] != 0: 31 | # print(i, j, k) 32 | ans_dist[i,j,k] += S[i,j,k] 33 | num_dist[i+j+k] += S[i,j,k] 34 | E += (i + j + k) * S[i,j,k] 35 | S[i,j,k] = 0 # 虽然是多余的还是可以写在这hhh 36 | continue 37 | # 定轨A进行抽取的情况 38 | if a-i >= b-j: 39 | N[i+1,j,k] += (3/8)*S[i,j,k] 40 | N[i+1,j+1,k] += (9/64)*S[i,j,k] 41 | N[i+1,j,k+1] += (1/8)*S[i,j,k] 42 | N[i+1,j+2,k] += (9/64)*S[i,j,k] 43 | N[i+1,j+1,k+1] += (7/32)*S[i,j,k] 44 | # 定轨B进行抽取的情况 45 | if a-i < b-j: 46 | N[i,j+1,k] += (3/8)*S[i,j,k] 47 | N[i+1,j+1,k] += (9/64)*S[i,j,k] 48 | N[i,j+1,k+1] += (1/8)*S[i,j,k] 49 | N[i+2,j+1,k] += (9/64)*S[i,j,k] 50 | N[i+1,j+1,k+1] += (7/32)*S[i,j,k] 51 | # 完成一轮后 52 | S = N 53 | N = 0 * N 54 | if abs(np.sum(ans_dist)-1)> 0.00001: 55 | print("ERROR: sum of ans is not equal to 1!", np.sum(ans_dist)) 56 | exit() 57 | return FiniteDist(np.trim_zeros(num_dist, 'b')) 58 | 59 | 60 | def calc_EP_weapon_simple(a, b): 61 | ''' 62 | 返回原神5.0版本后同卡池抽A限定五星武器a个和B限定五星武器b个所需五星个数分布 63 | 64 | 计算5.0后命定值为1的情况下,恰好花费k个五星后恰好抽到a个A限定UP五星和b个B限定UP五星的概率, 65 | 抽取时采用简单策略,定轨当前离要求数量较远的一个限定UP五星。 66 | ''' 67 | # S 第0维表示获得A的数量 第1维表示获得B的数量 第2维表示获得常驻的数量 68 | S = np.zeros((60, 60, 60)) 69 | S[(0, 0, 0)] = 1 70 | N = np.zeros((60, 60, 60)) 71 | # 记录最后到达每个状态概率 72 | ans_dist = np.zeros((60, 60, 60)) 73 | num_dist = np.zeros(200) 74 | # 控制状态更新,注意同样的状态可以在不同轮进行反复更新 75 | 76 | E = 0 # 期望值 77 | # 枚举定轨轮数 78 | for ep in range(2*(a+b)+1): # 此处+1是为了扫尾 79 | for i in range(a+b+1): # 获得A的数量 80 | for j in range(a+b+1): # 获得B的数量 81 | for k in range(a+b+1): # 获得常驻的数量 82 | # 若已经满足条件,则不继续 83 | if i>=a and j >=b: 84 | ans_dist[i,j,k] += S[i,j,k] 85 | num_dist[i+j+k] += S[i,j,k] 86 | E += (i + j + k) * S[i,j,k] 87 | S[i,j,k] = 0 # 虽然是多余的还是可以写在这hhh 88 | continue 89 | # 定轨A进行抽取的情况 90 | if a-i >= b-j: 91 | N[i+1,j,k] += (3/8)*S[i,j,k] 92 | N[i+1,j+1,k] += (3/8)*S[i,j,k] 93 | N[i+1,j,k+1] += (1/4)*S[i,j,k] 94 | # 定轨B进行抽取的情况 95 | if a-i < b-j: 96 | N[i,j+1,k] += (3/8)*S[i,j,k] 97 | N[i+1,j+1,k] += (3/8)*S[i,j,k] 98 | N[i,j+1,k+1] += (1/4)*S[i,j,k] 99 | # 完成一轮后 100 | S = N 101 | N = 0 * N 102 | if abs(np.sum(ans_dist)-1)> 0.00001: 103 | print("ERROR: sum of ans is not equal to 1!", np.sum(ans_dist)) 104 | exit() 105 | return FiniteDist(np.trim_zeros(num_dist, 'b')) 106 | 107 | if __name__ == '__main__': 108 | pass -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/get_cost.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | ''' 4 | 估算免费抽数模型建立在 https://bbs.nga.cn/read.php?tid=29533283 的统计数据基础上 5 | ''' 6 | 7 | # 计算免费获得抽数,做了一点平滑处理 8 | def get_free_pulls(days): 9 | # 这里默认计算0.928的discount 10 | discount = 0.928 11 | return (8+20+days*1.76235 + 260*(1-math.exp(days*-math.log(2)/30)))/discount 12 | 13 | # 获取购买部分抽数的价格 14 | def get_gacha_price(pulls, days): 15 | price = 0 16 | discount = 0.865 17 | 18 | # 小月卡 19 | per_pull_price = 1.6 20 | buy_pulls = (days * 100/160) 21 | if buy_pulls >= pulls: 22 | price += pulls * per_pull_price 23 | return price 24 | pulls -= buy_pulls 25 | price += buy_pulls * per_pull_price 26 | # 大月卡 27 | per_pull_price = 8.24 28 | buy_pulls = (days * (680/160+5)/45) 29 | if buy_pulls >= pulls: 30 | price += pulls * per_pull_price 31 | return price 32 | pulls -= buy_pulls 33 | price += buy_pulls * per_pull_price 34 | # 双倍首充 只按重置一轮估算 35 | per_pull_price = 8 36 | buy_pulls = 163.5 37 | if buy_pulls >= pulls: 38 | price += pulls * per_pull_price 39 | return price 40 | pulls -= buy_pulls 41 | price += buy_pulls * per_pull_price 42 | # 剩下的按照648算 43 | price += 12.83*discount*pulls 44 | 45 | return price 46 | 47 | # 获得氪金部分花费 48 | def get_gacha_cost(pulls, days): 49 | free_pulls = get_free_pulls(days) 50 | if free_pulls > pulls: 51 | return 0 52 | pulls -= free_pulls 53 | return get_gacha_price(pulls, days) 54 | 55 | # 获得买体力部分花费 56 | def get_resin_cost(cost_per_day, days): 57 | return 648*(cost_per_day*days/8080) 58 | 59 | if __name__ == '__main__': 60 | play_days = 365+2*30 # 游玩时间 61 | # print('免费获取部分', get_free_pulls(play_days)) 62 | resin_cost_per_day = 800 # 每日体力消耗原石 63 | get_charactors = 74*1.5 # 总角色数量 64 | get_weapons = 147 # 总武器数量 65 | # 估算的总抽数(当然这里换成准确值算的更准) 66 | tot_pulls = int(get_charactors*62.297+get_weapons*53.25-(195/365)*play_days) # 这里扣除了常驻送抽 67 | print('估计花销', get_gacha_cost(tot_pulls, play_days)+get_resin_cost(resin_cost_per_day, play_days)) -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/predict_next_type.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import GGanalysis.games.genshin_impact as GI 3 | from matplotlib import pyplot as plt 4 | 5 | # 计算常驻类别概率 6 | def calc_type_P(item_pity, not_met): 7 | # c = ggl.Pity5starCommon() 8 | # dist = c.conditional_distribution(1, item_pity) 9 | dist = GI.common_5star(1, item_pity=item_pity) 10 | # 计算单抽类别概率 11 | def pull_type_P(pull_num): 12 | A = 30 13 | B = 30 14 | if pull_num > 147: 15 | B += 300*(pull_num-147) 16 | # 轮盘选择法截断了 17 | if A+B > 10000: 18 | # print(B, pull_num) 19 | return min(10000, B)/10000 20 | # 轮盘选择法没有截断 21 | return B/(A+B) 22 | ans_P = 0 23 | for i in range(1, len(dist)): 24 | ans_P += pull_type_P(not_met+i)*dist[i] 25 | return ans_P 26 | 27 | 28 | # 绘制概率图 29 | x = np.linspace(0, 180, 181) 30 | y = np.linspace(0, 89, 90) 31 | X, Y = np.meshgrid(x, y) 32 | p_map = np.zeros((90, 181), dtype=float) 33 | for i in range(0, 90): 34 | for j in range(0, 181): 35 | p_map[i][j] = calc_type_P(i, j) 36 | 37 | X = X[:, 50:] 38 | Y = Y[:, 50:] 39 | p_map = p_map[:, 50:] 40 | 41 | # 绘图部分 42 | fig, ax = plt.subplots(dpi=150, figsize=(15, 5)) 43 | ax.set_xticks(range(50, 190, 5)) 44 | ax.grid(visible=True, which='major', color='k', alpha=0.2, linewidth=1) 45 | ax.grid(visible=True, which='minor', color='k', alpha=0.2, linewidth=0.5) 46 | plt.minorticks_on() 47 | ax.contourf(X, Y, p_map, [0.5001, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, 1], cmap=plt.cm.PuBu) 48 | ax.contour(X, Y, p_map, [0.5001, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99], colors='k', linestyles='--', linewidths=1) 49 | 50 | # 添加概率分界文字 51 | # line = ax.contour(X, Y, p_map, [0.5001, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99], colors='k', linestyles='--', linewidths=1) 52 | # ax.clabel(line, inline=True, fontsize=7) 53 | # 保存或显示图片 54 | # fig.savefig('v220824_img.svg') 55 | plt.show() -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/readme.md: -------------------------------------------------------------------------------- 1 | ## 采用模型 2 | 3 | 工具包里原神模块基于的抽卡模型见[原神抽卡全机制总结](https://bbs.nga.cn/read.php?tid=26754637),是非常准确的模型。为了实现方便,工具包中对UP四星角色、UP四星武器、UP五星武器时不考虑从常驻中歪到的情况,计算四星物品时忽略四星物品被五星物品顶到下一抽的情况。这些近似实际影响很小。 4 | 5 | 绘制概率图表见[原神抽卡概率工具表](https://bbs.nga.cn/read.php?tid=28026734) 6 | 7 | ## 其他 8 | 9 | 写了一个估算总氪金数的小程序`GetCost.py`,比较粗糙而且没怎么检查,可以用着玩一下,以后会仔细想想放哪里。 10 | 11 | 还有一个在测试的`PredictNextType.py`用于计算普池下一个五星是角色还是武器,会给出概率,还在观察有无BUG,也是可以玩一下的玩意。 12 | -------------------------------------------------------------------------------- /GGanalysis/games/genshin_impact/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.genshin_impact import PITY_5STAR, PITY_4STAR, PITY_W5STAR, PITY_W4STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在五星四星耦合情况下的概率 5 | common_gacha_system = PriorityPitySystem([PITY_5STAR, PITY_4STAR, [0, 1]]) 6 | print('常驻及角色池概率', common_gacha_system.get_stationary_p()) 7 | weapon_gacha_system = PriorityPitySystem([PITY_W5STAR, PITY_W4STAR, [0, 1]]) 8 | print('武器池概率', weapon_gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/girls_frontline2_exilium/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/girls_frontline2_exilium/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.girls_frontline2_exilium as GF2 2 | from GGanalysis.gacha_plot import QuantileFunction 3 | import matplotlib.cm as cm 4 | import numpy as np 5 | import time 6 | 7 | # 绘制少女前线2:追放抽卡概率分位图表 8 | 9 | # 定义获取物品个数的别名 10 | def gf2_character(x): 11 | return str(x-1)+'锥' 12 | def gf2_weapon(x): 13 | return str(x)+'校' 14 | 15 | # 少前2追放UP精英角色 16 | GF2_fig = QuantileFunction( 17 | GF2.up_elite(item_num=7, multi_dist=True), 18 | title='少前2追放UP精英人形抽取概率', 19 | item_name='UP精英人形', 20 | text_head='本算例中UP物品均不在常驻祈愿中', 21 | text_tail='采用GGanalysis库绘制 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 22 | max_pull=900, 23 | line_colors=cm.Oranges(np.linspace(0.3, 0.9, 7+1)), 24 | mark_func=gf2_character, 25 | is_finite=True) 26 | GF2_fig.show_figure(dpi=300, savefig=True) 27 | 28 | # 少前2追放特定UP标准角色 29 | GF2_fig = QuantileFunction( 30 | GF2.up_common_specific_character(item_num=7, multi_dist=True), 31 | title='少前2追放特定UP标准人形抽取概率', 32 | item_name='特定UP标准人形', 33 | text_head='本算例中UP物品均不在常驻祈愿中\n绘图曲线忽略精英与标准耦合情况', 34 | text_tail='采用GGanalysis库绘制 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 35 | max_pull=1200, 36 | mark_func=gf2_character, 37 | line_colors=cm.Purples(np.linspace(0.5, 0.9, 7+1)), 38 | is_finite=False) 39 | GF2_fig.show_figure(dpi=300, savefig=True) 40 | 41 | # 少前2追放定轨UP精英武器 42 | GF2_fig = QuantileFunction( 43 | GF2.up_elite_weapon(item_num=6, multi_dist=True), 44 | title='少前2追放UP精英武器抽取概率', 45 | item_name='UP精英武器', 46 | text_head='本算例中UP物品均不在常驻祈愿中', 47 | text_tail='采用GGanalysis库绘制 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 48 | max_pull=600, 49 | mark_func=gf2_weapon, 50 | line_colors=cm.Reds(np.linspace(0.5, 0.9, 6+1)), 51 | is_finite=True) 52 | GF2_fig.show_figure(dpi=300, savefig=True) 53 | 54 | # 少前2追放特定UP标准武器 55 | GF2_fig = QuantileFunction( 56 | GF2.up_common_specific_weapon(item_num=6, multi_dist=True), 57 | title='少前2追放特定UP标准武器抽取概率', 58 | item_name='特定UP标准武器', 59 | text_head='本算例中UP物品均不在常驻祈愿中\n绘图曲线忽略精英与标准耦合情况', 60 | text_tail='采用GGanalysis库绘制 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 61 | max_pull=500, 62 | mark_func=gf2_weapon, 63 | is_finite=False) 64 | GF2_fig.show_figure(dpi=300, savefig=True) -------------------------------------------------------------------------------- /GGanalysis/games/girls_frontline2_exilium/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | __all__ = [ 6 | 'PITY_ELITE', 7 | 'PITY_COMMON', 8 | 'PITY_ELITE_W', 9 | 'PITY_COMMON_W', 10 | 'common_elite', 11 | 'common_common', 12 | 'weapon_elite', 13 | 'weapon_common', 14 | 'up_elite', 15 | 'up_common_character', 16 | 'up_common_specific_character', 17 | 'up_elite_weapon', 18 | 'up_common_weapon', 19 | 'up_common_specific_weapon', 20 | ] 21 | 22 | # 少女前线2:追放普通精英道具保底概率表(推测值) 23 | PITY_ELITE = np.zeros(81) 24 | PITY_ELITE[1:59] = 0.006 25 | PITY_ELITE[59:81] = np.arange(1, 23) * 0.047 + 0.006 26 | PITY_ELITE[80] = 1 27 | # 少女前线2:追放普通标准道具保底概率表(推测值) 28 | PITY_COMMON = np.zeros(11) 29 | PITY_COMMON[1:10] = 0.06 30 | PITY_COMMON[10] = 1 31 | # 少女前线2:追放普通旧式武器概率 32 | P_OLD = 0.934 33 | 34 | # 少女前线2:追放普通精英道具保底概率表(推测值) 35 | PITY_ELITE_W = np.zeros(71) 36 | PITY_ELITE_W[1:51] = 0.007 37 | PITY_ELITE_W[51:71] = np.arange(1, 21) * 0.053 + 0.007 38 | PITY_ELITE_W[70] = 1 39 | # 少女前线2:追放普通标准道具保底概率表(推测值) 40 | PITY_COMMON_W = np.zeros(11) 41 | PITY_COMMON_W[1:10] = 0.07 42 | PITY_COMMON_W[10] = 1 43 | 44 | 45 | # 定义获取星级物品的模型 46 | common_elite = PityModel(PITY_ELITE) 47 | common_common = PityModel(PITY_COMMON) 48 | weapon_elite = PityModel(PITY_ELITE_W) 49 | weapon_common = PityModel(PITY_COMMON_W) 50 | 51 | 52 | # 定义获取UP物品模型,以下为简单推测模型 53 | up_elite = DualPityModel(PITY_ELITE, [0, 0.5, 1]) 54 | up_common_character = PityBernoulliModel(PITY_COMMON, p=0.25) 55 | up_common_specific_character = PityBernoulliModel(PITY_COMMON, p=0.25/2) 56 | up_elite_weapon = DualPityModel(PITY_ELITE_W, [0, 0.75, 1]) 57 | up_common_weapon = PityBernoulliModel(PITY_COMMON_W, p=0.75) 58 | up_common_specific_weapon = PityBernoulliModel(PITY_COMMON_W, p=0.75/3) 59 | 60 | if __name__ == '__main__': 61 | # print(PITY_ELITE[58:]) 62 | print("精英人形期望与综合概率", common_elite(1).exp, 1/common_elite(1).exp) 63 | print("限定精英人形期望与综合概率",up_elite(1).exp, 1/up_elite(1).exp) 64 | # print(PITY_COMMON) 65 | # print(common_common(1).exp, 1/common_common(1).exp) 66 | # print(PITY_ELITE[50:]) 67 | print("精英武器期望与综合概率",weapon_elite(1).exp, 1/weapon_elite(1).exp) 68 | print("限定精英武器期望与综合概率",up_elite_weapon(1).exp, 1/up_elite_weapon(1).exp) 69 | # print(PITY_COMMON_W) 70 | # print(weapon_common(1).exp, 1/weapon_common(1).exp) 71 | print("单体UP标准角色期望与综合概率",up_common_specific_character(1).exp, 1/up_common_specific_character(1).exp) 72 | print("单体UP标准武器期望与综合概率",up_common_specific_weapon(1).exp, 1/up_common_specific_weapon(1).exp) 73 | pass -------------------------------------------------------------------------------- /GGanalysis/games/girls_frontline2_exilium/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.girls_frontline2_exilium import * 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在精英道具耦合普通道具情况下的概率,精英道具不会重置普通道具保底 5 | gacha_system = PriorityPitySystem([PITY_ELITE, PITY_COMMON], remove_pity=False) 6 | print('精英道具及普通道具平稳概率', gacha_system.get_stationary_p()) 7 | print('普通道具的抽数分布', gacha_system.get_type_distribution(type=1)) 8 | 9 | # 调用预置工具计算武器池在精英道具耦合普通道具情况下的概率,精英道具不会重置普通道具保底 10 | gacha_system = PriorityPitySystem([PITY_ELITE_W, PITY_COMMON_W], remove_pity=False) 11 | print('武器池精英道具及普通道具平稳概率', gacha_system.get_stationary_p()) 12 | print('武器池普通道具的抽数分布', gacha_system.get_type_distribution(type=1)) -------------------------------------------------------------------------------- /GGanalysis/games/honkai_impact_3rd_v2/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/honkai_impact_3rd_v2/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | # 这里是没有什么东西的荒原喵~ 模型没做好 6 | __all__ = [ 7 | ] 8 | 9 | # 崩坏3第二部扩充补给S级保底概率表(推测值) 10 | PITY_S = np.zeros(91) 11 | PITY_S[1:90] = 0.0072 12 | PITY_S[90] = 1 13 | 14 | # 定义获取各等级物品的模型 15 | s_character = PityModel(PITY_S) 16 | 17 | 18 | if __name__ == '__main__': 19 | print(s_character(1).exp, 1/s_character(1).exp) 20 | pass -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * 2 | from .relic_model import * -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/gacha_model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 本模块五星及四星概率上升模型可信度很高,其余部分可能存在没有完全覆盖的机制 3 | ''' 4 | from GGanalysis.distribution_1d import * 5 | from GGanalysis.gacha_layers import * 6 | from GGanalysis.basic_models import * 7 | 8 | __all__ = [ 9 | 'PITY_5STAR', 10 | 'PITY_4STAR', 11 | 'PITY_W5STAR', 12 | 'PITY_W4STAR', 13 | 'common_5star', 14 | 'common_4star', 15 | 'up_5star_character', 16 | 'up_4star_character', 17 | 'up_4star_specific_character', 18 | 'common_5star_weapon', 19 | 'common_4star_weapon', 20 | 'up_5star_weapon', 21 | 'up_4star_weapon', 22 | 'up_4star_specific_weapon', 23 | ] 24 | 25 | # 星穹铁道普通5星保底概率表 26 | PITY_5STAR = np.zeros(91) 27 | PITY_5STAR[1:74] = 0.006 28 | PITY_5STAR[74:90] = np.arange(1, 17) * 0.06 + 0.006 29 | PITY_5STAR[90] = 1 30 | # 星穹铁道普通4星保底概率表 31 | PITY_4STAR = np.zeros(11) 32 | PITY_4STAR[1:9] = 0.051 33 | PITY_4STAR[9] = 0.051 + 0.51 34 | PITY_4STAR[10] = 1 35 | # 星穹铁道武器池5星保底概率表 36 | PITY_W5STAR = np.zeros(81) 37 | PITY_W5STAR[1:66] = 0.008 38 | PITY_W5STAR[66:80] = np.arange(1, 15) * 0.07 + 0.008 39 | PITY_W5STAR[80] = 1 40 | # 星穹铁道武器池4星保底概率表 41 | PITY_W4STAR = np.array([0,0.066,0.066,0.066,0.066,0.066,0.066,0.066,0.466,0.866,1]) 42 | 43 | # 定义获取星级物品的模型 44 | common_5star = PityModel(PITY_5STAR) 45 | common_4star = PityModel(PITY_4STAR) 46 | # 定义星穹铁道角色池模型 47 | up_5star_character = DualPityModel(PITY_5STAR, [0, 0.5 + 0.5/8, 1]) 48 | up_4star_character = DualPityModel(PITY_4STAR, [0, 0.5, 1]) 49 | up_4star_specific_character = DualPityBernoulliModel(PITY_4STAR, [0, 0.5, 1], 1/3) 50 | # 定义星穹铁道武器池模型 51 | common_5star_weapon = PityModel(PITY_W5STAR) 52 | common_4star_weapon = PityModel(PITY_W4STAR) 53 | up_5star_weapon = DualPityModel(PITY_W5STAR, [0, 0.75 + 0.25/8, 1]) 54 | up_4star_weapon = DualPityModel(PITY_W4STAR, [0, 0.75, 1]) 55 | up_4star_specific_weapon = DualPityBernoulliModel(PITY_W4STAR, [0, 0.75, 1], 1/3) 56 | 57 | 58 | if __name__ == '__main__': 59 | print("UP五星角色期望", up_5star_character(1).exp) 60 | print("UP五星光锥期望", up_5star_weapon(1).exp) 61 | # 计算分位点 62 | # quantile_pos = [0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] 63 | # print("选择分位点"+str(quantile_pos)) 64 | # for c in range(0, 7): 65 | # for w in range(0, 6): 66 | # dist = up_5star_character(c) * up_5star_weapon(w) 67 | # print(f"{c}魂{w}叠 "+str(dist.quantile_point(quantile_pos))) -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/relic_data.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 崩铁遗器数值表 3 | ''' 4 | RELIC_TYPES = ['head', 'hands', 'body', 'feet', 'link rope', 'planar sphere'] 5 | 6 | RELIC_SETS = { 7 | 'cavern relics': '遗器套装', 8 | 'planar ornaments': '位面饰品', 9 | } 10 | 11 | CAVERN_RELICS = ['head', 'hands', 'body', 'feet'] 12 | PLANAR_ORNAMENTS = ['link rope', 'planar sphere'] 13 | 14 | RELIC_NAME = { 15 | 'head': '头部', 16 | 'hands': '手部', 17 | 'body': '躯干', 18 | 'feet': '脚部', 19 | 'planar sphere': '位面球', 20 | 'link rope': '连结绳', 21 | } 22 | 23 | # 类型别名对照表 24 | STAT_NAME = { 25 | 'hp': '数值生命值', 26 | 'atk': '数值攻击力', 27 | 'def': '数值防御力', 28 | 'hpp': '百分比生命值', 29 | 'atkp': '百分比攻击力', 30 | 'defp': '百分比防御力', 31 | 'speed': '速度', 32 | 'physicalDB': '物理伤害加成', 33 | 'fireDB': '火属性伤害加成', 34 | 'iceDB': '冰属性伤害加成', 35 | 'lightningDB': '雷属性伤害加成', 36 | 'windDB': '风属性伤害加成', 37 | 'quantumDB': '量子属性伤害加成', 38 | 'imaginaryDB': '虚数属性伤害加成', 39 | 'cr': '暴击率', 40 | 'cd': '暴击伤害', 41 | 'ehr': '效果命中', 42 | 'er': '效果抵抗', 43 | 'be': '击破特攻', 44 | 'err': '能量恢复效率', 45 | 'hb': '治疗量加成', 46 | } 47 | 48 | # 所有主词条掉落权重表 49 | W_MAIN_STAT = { 50 | 'head': {'hp': 1000}, 51 | 'hands': {'atk': 1000}, 52 | 'body': { 53 | 'hpp': 1000, 54 | 'atkp': 1000, 55 | 'defp': 1000, 56 | 'cr': 500, 57 | 'cd': 500, 58 | 'hb': 500, 59 | 'ehr': 500, 60 | }, 61 | 'feet': { 62 | 'hpp': 1375, 63 | 'atkp': 1500, 64 | 'defp': 1500, 65 | 'speed': 625, 66 | }, 67 | 'link rope': { 68 | 'hpp': 1375, 69 | 'atkp': 1375, 70 | 'defp': 1225, 71 | 'be': 750, 72 | 'err': 275, 73 | }, 74 | 'planar sphere': { 75 | 'hpp': 625, 76 | 'atkp': 625, 77 | 'defp': 600, 78 | 'physicalDB': 450, 79 | 'fireDB': 450, 80 | 'iceDB': 450, 81 | 'lightningDB': 450, 82 | 'windDB': 450, 83 | 'quantumDB': 450, 84 | 'imaginaryDB': 450, 85 | }, 86 | } 87 | 88 | # 所有副词条权重表 89 | W_SUB_STAT = { 90 | 'hp': 100, 91 | 'atk': 100, 92 | 'def': 100, 93 | 'hpp': 100, 94 | 'atkp': 100, 95 | 'defp': 100, 96 | 'speed': 40, 97 | 'cr': 60, 98 | 'cd': 60, 99 | 'ehr': 80, 100 | 'er': 80, 101 | 'be': 80, 102 | } 103 | 104 | # 五星遗器初始4词条概率 105 | P_INIT4_DROP = 0.2 106 | 107 | # 五星遗器主词条满级时属性 https://www.bilibili.com/video/BV11w411U7AV/ 108 | MAIN_STAT_MAX = { 109 | 'hp': 705.6, 110 | 'atk': 352.8, 111 | 'hpp': 0.432, 112 | 'atkp': 0.432, 113 | 'defp': 0.54, 114 | 'cr': 0.324, 115 | 'cd': 0.648, 116 | 'hb': 0.345606, 117 | 'ehr': 0.432, 118 | 'speed': 25.032, 119 | 'be': 0.648, 120 | 'err': 0.194394, 121 | 'physicalDB': 0.388803, 122 | 'fireDB': 0.388803, 123 | 'iceDB': 0.388803, 124 | 'lightningDB': 0.388803, 125 | 'windDB': 0.388803, 126 | 'quantumDB': 0.388803, 127 | 'imaginaryDB': 0.388803, 128 | } 129 | 130 | # 五星副词条单次升级最大值,升级挡位为三挡比例 8:9:10 https://honkai-star-rail.fandom.com/wiki/Relic/Stats 131 | SUB_STAT_MAX = { 132 | 'hp': 42.338, 133 | 'atk': 21.169, 134 | 'def': 21.169, 135 | 'hpp': 0.0432, 136 | 'atkp': 0.0432, 137 | 'defp': 0.054, 138 | 'speed': 2.6, 139 | 'cr': 0.0324, 140 | 'cd': 0.0648, 141 | 'ehr': 0.0432, 142 | 'er': 0.0432, 143 | 'be': 0.0648, 144 | } 145 | 146 | # 默认打分权重,这个值可以是 [0,1] 的浮点数 147 | DEFAULT_STAT_SCORE = { 148 | 'hp': 0, 149 | 'atk': 0, 150 | 'def': 0, 151 | 'hpp': 0, 152 | 'atkp': 0.5, 153 | 'defp': 0, 154 | 'speed': 1, 155 | 'cr': 1, 156 | 'cd': 1, 157 | 'ehr': 0, 158 | 'er': 0, 159 | 'be': 0, 160 | } 161 | 162 | # 默认主词条得分 163 | DEFAULT_MAIN_STAT_SCORE = { 164 | 'hp': 0, 165 | 'atk': 0, 166 | 'hpp': 0, 167 | 'atkp': 0, 168 | 'defp': 0, 169 | 'cr': 0, 170 | 'cd': 0, 171 | 'hb': 0, 172 | 'ehr': 0, 173 | 'speed': 0, 174 | 'be': 0, 175 | 'err': 0, 176 | 'physicalDB': 0, 177 | 'fireDB': 0, 178 | 'iceDB': 0, 179 | 'lightningDB': 0, 180 | 'windDB': 0, 181 | 'quantumDB': 0, 182 | 'imaginaryDB': 0, 183 | } 184 | 185 | # 默认主属性选择 186 | DEFAULT_MAIN_STAT = { 187 | 'head': 'hp', 188 | 'hands': 'atk', 189 | 'body': 'cr', 190 | 'feet': 'speed', 191 | 'planar sphere': 'physicalDB', 192 | 'link rope': 'atkp', 193 | } 194 | 195 | # 默认颜色 196 | DEFAULT_STAT_COLOR = { 197 | 'hp': '#4e8046', 198 | 'atk': '#8c4646', 199 | 'def': '#8c8c3f', 200 | 'hpp': '#65a65b', 201 | 'atkp': '#b35959', 202 | 'defp': '#b2b350', 203 | 'speed': '#a15ba6', 204 | 'physicalDB': '#7a8c99', 205 | 'fireDB': '#d96857', 206 | 'iceDB': '#6cb1d9', 207 | 'lightningDB': '#c566cc', 208 | 'windDB': '#4dbf99', 209 | 'quantumDB': '#712a79', 210 | 'imaginaryDB': '#edd92d', 211 | 'cr': '#f29224', 212 | 'cd': '#f24124', 213 | 'ehr': '#617349', 214 | 'er': '#563aa6', 215 | 'be': '#13eaed', 216 | 'err': '#665ba6', 217 | 'hb': '#79a63a', 218 | } -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/relic_model.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | from functools import lru_cache 3 | from typing import Callable 4 | 5 | import numpy as np 6 | import math 7 | import itertools 8 | 9 | from GGanalysis.games.honkai_star_rail.relic_data import * 10 | from GGanalysis.ScoredItem.genshin_like_scored_item import * 11 | from GGanalysis.ScoredItem.scored_item import ScoredItem, ScoredItemSet 12 | 13 | """ 14 | 崩坏:星穹铁道遗器类 15 | """ 16 | __all__ = [ 17 | "StarRailRelic", 18 | "StarRailRelicSet", 19 | "StarRailCavernRelics", 20 | "StarRailPlanarOrnaments", 21 | "RELIC_TYPES", 22 | "CAVERN_RELICS", 23 | "PLANAR_ORNAMENTS", 24 | "STAT_NAME", 25 | "W_MAIN_STAT", 26 | "W_SUB_STAT", 27 | "DEFAULT_MAIN_STAT", 28 | "DEFAULT_STAT_SCORE", 29 | "DEFAULT_STAT_COLOR", 30 | ] 31 | 32 | # 副词条档位 33 | SUB_STATS_RANKS = [8, 9, 10] 34 | # 权重倍数乘数,必须为整数,越大计算越慢精度越高 35 | RANK_MULTI = 1 36 | # 全局遗器副词条权重 37 | STATS_WEIGHTS = {} 38 | 39 | get_init_state = create_get_init_state(STATS_WEIGHTS, SUB_STATS_RANKS, RANK_MULTI) 40 | get_state_level_up = create_get_state_level_up(STATS_WEIGHTS, SUB_STATS_RANKS, RANK_MULTI) 41 | 42 | def set_using_weight(new_weight: dict): 43 | """更换采用权重时要刷新缓存,注意得分权重必须小于等于1""" 44 | global STATS_WEIGHTS 45 | global get_init_state 46 | global get_state_level_up 47 | STATS_WEIGHTS = new_weight 48 | # print('Refresh weight cache!') 49 | get_init_state = create_get_init_state(STATS_WEIGHTS, SUB_STATS_RANKS, RANK_MULTI) 50 | get_state_level_up = create_get_state_level_up(STATS_WEIGHTS, SUB_STATS_RANKS, RANK_MULTI) 51 | 52 | def dict_weight_sum(weights: dict): 53 | """获得字典中所有值的和""" 54 | return sum(weights.values()) 55 | 56 | def get_combinations_p(stats_p: dict, select_num=4): 57 | """计算获得拥有4个副词条的五星遗器不同副词条组合的概率""" 58 | ans = {} 59 | weight_all = dict_weight_sum(stats_p) 60 | for perm in itertools.permutations(list(stats_p.keys()), select_num): 61 | # 枚举并计算该排列的出现概率 62 | p, s = 1, weight_all 63 | for m in perm: 64 | p *= stats_p[m] / s 65 | s -= stats_p[m] 66 | # 排序得到键,保证每种组合都是唯一的 67 | perm_key = tuple(sorted(perm)) 68 | if perm_key in ans: 69 | ans[perm_key] += p 70 | else: 71 | ans[perm_key] = p 72 | return ans 73 | 74 | class StarRailRelic(ScoredItem): 75 | """崩铁遗器类""" 76 | def __init__( 77 | self, 78 | type: str = "hands", # 道具类型 79 | type_p = 1/4, # 每次获得道具是是类型道具概率 80 | main_stat: str = None, # 主词条属性 81 | sub_stats_select_weight: dict = W_SUB_STAT, # 副词条抽取权重 82 | main_stat_score: dict = DEFAULT_MAIN_STAT_SCORE, # 主词条默认计算属性 83 | stats_score: dict = DEFAULT_STAT_SCORE, # 词条评分权重 84 | p_4sub: float = P_INIT4_DROP, # 初始4词条掉落率 85 | sub_stats_filter: Callable[..., bool] = None, # 设定副词条过滤器,若函数判断False直接丢掉被过滤的情况 86 | forced_combinations = None, # 直接设定初始词条组合 87 | ) -> None: 88 | # 计算获得主词条概率 89 | self.type = type 90 | self.main_stat_score = main_stat_score 91 | if main_stat is not None: 92 | self.main_stat = main_stat 93 | else: 94 | self.main_stat = DEFAULT_MAIN_STAT[self.type] 95 | drop_p = ( 96 | type_p 97 | * W_MAIN_STAT[self.type][self.main_stat] 98 | / dict_weight_sum(W_MAIN_STAT[self.type]) 99 | ) 100 | # 确定可选副词条 101 | self.sub_stats_weight = deepcopy(sub_stats_select_weight) 102 | if self.main_stat in self.sub_stats_weight: 103 | del self.sub_stats_weight[self.main_stat] 104 | # 确定副词条四件概率 105 | self.p_4sub = p_4sub 106 | # 词条权重改变时应清除缓存 107 | self.stats_score = stats_score 108 | if self.stats_score != STATS_WEIGHTS: 109 | set_using_weight(self.stats_score) 110 | # 计算副词条组合概率 111 | ans = ScoredItem() 112 | if forced_combinations is None: 113 | self.sub_stats_combinations = get_combinations_p( 114 | stats_p=self.sub_stats_weight, select_num=4 115 | ) 116 | else: 117 | self.sub_stats_combinations = forced_combinations 118 | # 遍历所有情况累加,满级15级,每3级强化一次,初始4可以强化5次 119 | for stat_comb in list(self.sub_stats_combinations.keys()): 120 | if sub_stats_filter is not None: 121 | if sub_stats_filter(stat_comb) is False: 122 | continue 123 | temp_base = get_init_state(stat_comb, init_score=self.main_stat_score[self.main_stat]) 124 | temp_level_up = get_state_level_up(stat_comb) 125 | # 初始3词条和初始四词条的情况 126 | temp_3 = ( 127 | temp_base 128 | * temp_level_up 129 | * temp_level_up 130 | * temp_level_up 131 | * temp_level_up 132 | ) 133 | temp_4 = temp_3 * temp_level_up 134 | ans += ( 135 | (1 - self.p_4sub) * temp_3 + self.p_4sub * temp_4 136 | ) * self.sub_stats_combinations[stat_comb] 137 | super().__init__(ans.score_dist, ans.sub_stats_exp, drop_p, stats_score=self.stats_score) 138 | 139 | class StarRailRelicSet(ScoredItemSet): 140 | def __init__( 141 | self, 142 | main_stat: dict = DEFAULT_MAIN_STAT, 143 | main_stat_score: dict = DEFAULT_MAIN_STAT_SCORE, 144 | stats_score: dict = DEFAULT_STAT_SCORE, 145 | set_types: list = CAVERN_RELICS, 146 | p_4sub: float = P_INIT4_DROP, # 根据圣遗物掉落来源确定4件套掉落率 147 | type_p = 1/8, # 掉落对应套装部位的概率 148 | sub_stats_filter: Callable[..., bool] = None, # 设定副词条过滤器,若函数判断False直接丢掉被过滤的情况 149 | ) -> None: 150 | # 初始化道具 151 | self.main_stat = main_stat 152 | self.main_stat_score = main_stat_score 153 | self.stats_score = stats_score 154 | self.p_4sub = p_4sub 155 | self.set_types = set_types 156 | item_set = {} 157 | for type in set_types: 158 | item_set[type] = StarRailRelic( 159 | type=type, 160 | type_p=type_p, 161 | main_stat=main_stat[type], 162 | main_stat_score=main_stat_score, 163 | stats_score=stats_score, 164 | p_4sub=self.p_4sub, 165 | sub_stats_filter=sub_stats_filter, 166 | ) 167 | super().__init__(item_set) 168 | 169 | class StarRailCavernRelics(StarRailRelicSet): 170 | # 崩铁遗器四件套 171 | def __init__( 172 | self, 173 | main_stat: dict = DEFAULT_MAIN_STAT, 174 | main_stat_score: dict = DEFAULT_MAIN_STAT_SCORE, 175 | stats_score: dict = DEFAULT_STAT_SCORE, 176 | set_types: list = CAVERN_RELICS, 177 | p_4sub: float = P_INIT4_DROP, # 根据圣遗物掉落来源确定4件套掉落率 178 | type_p = 1/8, # 掉落对应套装部位的概率 179 | sub_stats_filter: Callable[..., bool] = None, # 设定副词条过滤器,若函数判断False直接丢掉被过滤的情况 180 | ) -> None: 181 | super().__init__( 182 | main_stat = main_stat, 183 | main_stat_score = main_stat_score, 184 | stats_score = stats_score, 185 | set_types = set_types, 186 | p_4sub = p_4sub, 187 | type_p = type_p, 188 | sub_stats_filter = sub_stats_filter, 189 | ) 190 | 191 | class StarRailPlanarOrnaments(StarRailRelicSet): 192 | # 崩铁遗器两件套 193 | def __init__( 194 | self, 195 | main_stat: dict = DEFAULT_MAIN_STAT, 196 | main_stat_score: dict = DEFAULT_MAIN_STAT_SCORE, 197 | stats_score: dict = DEFAULT_STAT_SCORE, 198 | set_types: list = PLANAR_ORNAMENTS, 199 | p_4sub: float = P_INIT4_DROP, # 根据圣遗物掉落来源确定4件套掉落率 200 | type_p = 1/4, # 掉落对应套装部位的概率 201 | sub_stats_filter: Callable[..., bool] = None, # 设定副词条过滤器,若函数判断False直接丢掉被过滤的情况 202 | ) -> None: 203 | super().__init__( 204 | main_stat = main_stat, 205 | main_stat_score = main_stat_score, 206 | stats_score = stats_score, 207 | set_types = set_types, 208 | p_4sub = p_4sub, 209 | type_p = type_p, 210 | sub_stats_filter = sub_stats_filter, 211 | ) 212 | 213 | if __name__ == "__main__": 214 | pass -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/relic_sim.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.SimulationTools.scored_item_sim import HoyoItemSim, HoyoItemSetSim 2 | from GGanalysis.games.honkai_star_rail.relic_data import W_MAIN_STAT,W_SUB_STAT,CAVERN_RELICS,PLANAR_ORNAMENTS,DEFAULT_MAIN_STAT,DEFAULT_STAT_SCORE 3 | 4 | class StarRailRelicSim(HoyoItemSim): 5 | # 崩铁遗器单件 6 | def __init__(self, stat_score, item_type, main_stat) -> None: 7 | super().__init__( 8 | stat_score, 9 | item_type, 10 | main_stat, 11 | W_MAIN_STAT, 12 | W_SUB_STAT, 13 | [0,0,0,0,0.8,0.2], 14 | [0,0,0,0,0,0,0,0,1/3,1/3,1/3] 15 | ) 16 | 17 | class StarRailRelicCavernRelicsSim(HoyoItemSetSim): 18 | # 崩铁遗器四件套 19 | def __init__(self, main_stats, stat_score) -> None: 20 | items = [] 21 | items_p = [1/(len(CAVERN_RELICS))] * len(CAVERN_RELICS) 22 | for item_type in CAVERN_RELICS: 23 | items.append(StarRailRelicSim(stat_score, item_type, main_stats[item_type])) 24 | super().__init__(items, items_p, 1/2, stat_score) 25 | 26 | class StarRailRelicPlanarOrnamentsSim(HoyoItemSetSim): 27 | # 崩铁遗器两件套 28 | def __init__(self, main_stats, stat_score) -> None: 29 | items = [] 30 | items_p = [1/(len(PLANAR_ORNAMENTS))] * len(PLANAR_ORNAMENTS) 31 | for item_type in PLANAR_ORNAMENTS: 32 | items.append(StarRailRelicSim(stat_score, item_type, main_stats[item_type])) 33 | super().__init__(items, items_p, 1/2, stat_score) 34 | 35 | if __name__ == '__main__': 36 | # Windows 下 Python 多线程必须使用 "if __name__ == '__main__':" 37 | sim_players = 10000 38 | sim_4set = StarRailRelicCavernRelicsSim(DEFAULT_MAIN_STAT, DEFAULT_STAT_SCORE) 39 | score, sub_stat = sim_4set.parallel_sim_player_group(sim_players, 2000) 40 | # print(sim_4set.sample_best(2000)) 41 | print("Sim Players:", sim_players) 42 | print("Total Score:", score.mean/9) 43 | print({key:sub_stat[key].mean/9 for key in ['atkp', 'cr', 'cd', 'speed']}) 44 | print('Sub stat score sum', sum([sub_stat[key].mean/9*DEFAULT_STAT_SCORE[key] for key in DEFAULT_STAT_SCORE.keys()])) -------------------------------------------------------------------------------- /GGanalysis/games/honkai_star_rail/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.honkai_star_rail import PITY_5STAR, PITY_4STAR, PITY_W5STAR, PITY_W4STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在五星四星耦合情况下的概率 5 | common_gacha_system = PriorityPitySystem([PITY_5STAR, PITY_4STAR, [0, 1]]) 6 | print('常驻及角色池概率', common_gacha_system.get_stationary_p()) 7 | light_cone_gacha_system = PriorityPitySystem([PITY_W5STAR, PITY_W4STAR, [0, 1]]) 8 | print('光锥池概率',light_cone_gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/reverse_1999/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/reverse_1999/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.reverse_1999 as RV 2 | from GGanalysis.gacha_plot import QuantileFunction, DrawDistribution 3 | from GGanalysis import FiniteDist 4 | import matplotlib.cm as cm 5 | import numpy as np 6 | import time 7 | 8 | def RV_character(x): 9 | return '塑造'+str(x-1) 10 | def RV_collection(x): 11 | return '集齐'+str(x)+'种' 12 | 13 | # 重返未来1999 UP6星角色 14 | RV_fig = QuantileFunction( 15 | RV.up_6star(6, multi_dist=True), 16 | title='重返未来1999 UP六星角色抽取概率', 17 | item_name='UP六星角色', 18 | text_head='采用官方公示模型\n获取1个UP六星角色最多140抽', 19 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 20 | max_pull=750, 21 | mark_func=RV_character, 22 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 23 | y_base_gap=25, 24 | is_finite=None) 25 | RV_fig.show_figure(dpi=300, savefig=True) 26 | 27 | # 重返未来1999 UP5星角色 28 | ans_list = [FiniteDist()] 29 | for i in range(1, 7): 30 | ans_list.append(RV.specific_up_5star(i)) 31 | RV_fig = QuantileFunction( 32 | ans_list, 33 | title='重返未来1999 特定UP五星角色抽取概率', 34 | item_name='UP五星角色', 35 | text_head='采用官方公示模型', 36 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 37 | max_pull=750, 38 | mark_func=RV_character, 39 | line_colors=cm.GnBu(np.linspace(0.4, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 40 | y_base_gap=25, 41 | is_finite=False) 42 | RV_fig.show_figure(dpi=300, savefig=True) 43 | 44 | # 重返未来1999 常驻集齐UP6星角色 45 | ans_list = [FiniteDist()] 46 | for i in range(1, 12): 47 | ans_list.append(RV.stander_charactor_collection(target_types=i)) 48 | RV_fig = QuantileFunction( 49 | ans_list, 50 | title='重返未来1999集齐常驻6星角色概率', 51 | item_name='常驻6星角色', 52 | text_head='采用官方公示模型', 53 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 54 | max_pull=3800, 55 | mark_func=RV_collection, 56 | line_colors=cm.PuRd(np.linspace(0.2, 0.9, 11+1)), 57 | y_base_gap=25, 58 | mark_offset=-0.4, 59 | y2x_base=3, 60 | mark_exp=False, 61 | is_finite=False) 62 | RV_fig.show_figure(dpi=300, savefig=True) 63 | 64 | # 重返未来1999 集齐两个UP五星角色 65 | RV_fig = DrawDistribution( 66 | dist_data=RV.both_up_5star(), 67 | title='重返未来1999集齐两个UP五星角色', 68 | max_pull=300, 69 | text_head='采用官方公示模型', 70 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 71 | description_pos=200, 72 | is_finite=False, 73 | ) 74 | RV_fig.show_dist(dpi=300, savefig=True) 75 | 76 | # 重返未来1999 获取六星角色 77 | RV_fig = DrawDistribution( 78 | dist_data=RV.common_6star(1), 79 | title='重返未来1999获取六星角色', 80 | text_head='采用官方公示模型', 81 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 82 | is_finite=True, 83 | ) 84 | RV_fig.show_dist(dpi=300, savefig=True) 85 | 86 | # 重返未来1999 常驻获取特定六星角色 87 | RV_fig = DrawDistribution( 88 | dist_data=RV.specific_stander_6star(1), 89 | title='重返未来1999常驻获取特定六星角色', 90 | text_head='采用官方公示模型', 91 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 92 | max_pull=2300, 93 | description_pos=1550, 94 | quantile_pos=[0.25, 0.5, 0.75, 0.9, 0.99], 95 | is_finite=False, 96 | ) 97 | RV_fig.show_dist(dpi=300, savefig=True) 98 | 99 | # 重返未来1999 获取UP六星角色 100 | RV_fig = DrawDistribution( 101 | dist_data=RV.up_6star(1), 102 | title='重返未来1999获取UP六星角色', 103 | text_head='采用官方公示模型', 104 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 105 | is_finite=True, 106 | ) 107 | RV_fig.show_dist(dpi=300, savefig=True) 108 | 109 | # 重返未来1999 新春限定池获取UP六星角色 110 | RV_fig = QuantileFunction( 111 | RV.up_6star(6, multi_dist=True), 112 | title='重返未来1999新春限定六星角色概率(200井)', 113 | item_name='UP六星角色', 114 | text_head='采用官方公示模型\n获取1个UP六星角色最多140抽\n考虑全部兑换六星角色\n最多560抽满塑造', 115 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 116 | max_pull=560, 117 | mark_func=RV_character, 118 | line_colors=cm.YlOrBr(np.linspace(0.1, 0.95, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 119 | direct_exchange=200, 120 | y_base_gap=25, 121 | y2x_base=3, 122 | plot_direct_exchange=True, 123 | is_finite=False) 124 | RV_fig.show_figure(dpi=300, savefig=True) 125 | 126 | # 重返未来1999 自选双常驻六星池获取特定六星角色 127 | RV_fig = QuantileFunction( 128 | RV.dual_up_specific_6star(6, multi_dist=True), 129 | title='重返未来1999自选双常驻六星池获取特定六星角色', 130 | item_name='特定六星角色', 131 | text_head='采用官方公示模型\n获取1个任意选定六星角色最多140抽', 132 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 133 | max_pull=1400, 134 | mark_func=RV_character, 135 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 6+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 136 | y_base_gap=25, 137 | is_finite=False) 138 | RV_fig.show_figure(dpi=300, savefig=True) 139 | 140 | def collection_description( 141 | item_name='角色', 142 | cost_name='抽', 143 | text_head=None, 144 | mark_exp=None, 145 | direct_exchange=None, 146 | show_max_pull=None, 147 | is_finite=None, 148 | text_tail=None 149 | ): 150 | description_text = '' 151 | # 开头附加文字 152 | if text_head is not None: 153 | description_text += text_head 154 | # 对道具期望值的描述 155 | if mark_exp is not None: 156 | if description_text != '': 157 | description_text += '\n' 158 | description_text += '集齐两个自选'+item_name+'期望为'+format(mark_exp, '.2f')+cost_name 159 | # 末尾附加文字 160 | if text_tail is not None: 161 | if description_text != '': 162 | description_text += '\n' 163 | description_text += text_tail 164 | description_text = description_text.rstrip() 165 | return description_text 166 | 167 | # 重返未来1999 自选双常驻六星池集齐自选六星角色 168 | RV_fig = DrawDistribution( 169 | RV.dual_up_both_6star(), 170 | title='重返未来1999自选双常驻六星池集齐自选六星角色', 171 | text_tail='无法确保在有限抽数内一定集齐\n@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 172 | is_finite=False, 173 | max_pull=550, 174 | show_peak=False, 175 | description_func=collection_description, 176 | ) 177 | RV_fig.show_two_graph(dpi=300, savefig=True) -------------------------------------------------------------------------------- /GGanalysis/games/reverse_1999/gacha_model.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.distribution_1d import * 2 | from GGanalysis.gacha_layers import * 3 | from GGanalysis.basic_models import * 4 | 5 | __all__ = [ 6 | 'PITY_6STAR', 7 | 'P_5', 8 | 'P_4', 9 | 'P_3', 10 | 'P_2', 11 | 'P_6s', 12 | 'P_5s', 13 | 'P_4s', 14 | 'P_3s', 15 | 'P_2s', 16 | 'common_6star', 17 | 'common_5star', 18 | 'common_4star', 19 | 'common_3star', 20 | 'common_2star', 21 | 'up_6star', 22 | 'specific_up_5star', 23 | 'specific_stander_6star', 24 | 'both_up_5star', 25 | 'stander_charactor_collection', 26 | 'dual_up_specific_6star', 27 | 'dual_up_both_6star', 28 | 'triple_pool_6star', 29 | ] 30 | 31 | # 重返未来1999普通6星保底概率表 32 | PITY_6STAR = np.zeros(71) 33 | PITY_6STAR[1:61] = 0.015 34 | PITY_6STAR[61:71] = np.arange(1, 11) * 0.025 + 0.015 35 | PITY_6STAR[70] = 1 36 | # 其他无保底物品初始概率 37 | P_5 = 0.085 38 | P_4 = 0.4 39 | P_3 = 0.45 40 | P_2 = 0.05 41 | # 按照 stationary_p.py 的计算结果修订 42 | P_6s = 0.0235922 43 | P_5s = 0.08479687 44 | P_4s = 0.40130344 45 | P_3s = 0.44340267 46 | P_2s = 0.04690482 47 | # 定义获取星级物品的模型 48 | common_6star = PityModel(PITY_6STAR) 49 | common_5star = BernoulliGachaModel(P_5s) 50 | common_4star = BernoulliGachaModel(P_4s) 51 | common_3star = BernoulliGachaModel(P_3s) 52 | common_2star = BernoulliGachaModel(P_2s) 53 | # 定义获取UP物品模型 54 | up_6star = DualPityModel(PITY_6STAR, [0, 0.5, 1]) 55 | specific_up_5star = BernoulliGachaModel(P_5s/4) 56 | # 定义集齐两个UP五星模型 57 | both_up_5star = GeneralCouponCollectorModel([P_5s/4, P_5s/4], ['up5star1', 'up5star2']) 58 | # 常驻获取特定6星角色 59 | specific_stander_6star = PityBernoulliModel(PITY_6STAR, 1/11) 60 | # 定义集齐常驻 61 | stander_charactor_collection = PityCouponCollectorModel(PITY_6STAR, 11) 62 | # 常驻自选六星角色池抽到特定自选六星角色模型 63 | dual_up_specific_6star = DualPityBernoulliModel(PITY_6STAR, [0, 0.7, 1], 1/2) 64 | dual_up_both_6star = DualPityCouponCollectorModel(PITY_6STAR, [0, 0.7, 1], 2) 65 | # 常驻三池选一卡池模型 66 | triple_pool_6star = PityBernoulliModel(PITY_6STAR, 1/3) 67 | 68 | if __name__ == '__main__': 69 | # print(PITY_6STAR) 70 | print(common_6star(1).exp, 1/common_6star(1).exp) 71 | # print(stander_charactor_collection(target_types=2).exp) 72 | print(dual_up_specific_6star(1).exp) 73 | print(dual_up_both_6star().exp) -------------------------------------------------------------------------------- /GGanalysis/games/reverse_1999/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.reverse_1999 import PITY_6STAR, P_5, P_4, P_3, P_2 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具 5 | gacha_system = PriorityPitySystem([PITY_6STAR, [0, P_5], [0, P_4, P_4, P_4, P_4, P_4, P_4, P_4, P_4, P_4, 1], [0, P_3], [0, P_2]]) 6 | print(gacha_system.get_stationary_p()) -------------------------------------------------------------------------------- /GGanalysis/games/wuthering_waves/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/wuthering_waves/figure_plot.py: -------------------------------------------------------------------------------- 1 | import GGanalysis.games.wuthering_waves as WW 2 | from GGanalysis.gacha_plot import QuantileFunction, DrawDistribution 3 | from GGanalysis import FiniteDist 4 | import matplotlib.cm as cm 5 | import numpy as np 6 | import time 7 | 8 | def WW_item(x): 9 | return '抽取'+str(x)+'个' 10 | 11 | # 鸣潮5星物品分布 12 | WW_fig = DrawDistribution( 13 | dist_data=WW.common_5star(1), 14 | title='鸣潮获取五星物品抽数分布', 15 | text_head='采用推测模型,不保证完全准确', 16 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 17 | item_name='五星物品', 18 | quantile_pos=[0.1, 0.2, 0.3, 0.4, 0.5, 0.9, 0.99], 19 | is_finite=True, 20 | ) 21 | WW_fig.show_dist(dpi=300, savefig=True) 22 | 23 | # 鸣潮获取UP5星物品 24 | WW_fig = QuantileFunction( 25 | WW.common_5star(7, multi_dist=True), 26 | title='鸣潮获取五星道具概率', 27 | item_name='五星道具', 28 | text_head='采用推测模型,不保证完全准确\n获取1个五星道具最多80抽', 29 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 30 | max_pull=575, 31 | line_colors=cm.Oranges(np.linspace(0.5, 0.9, 7+1)), 32 | # y_base_gap=25, 33 | # y2x_base=2, 34 | is_finite=True) 35 | WW_fig.show_figure(dpi=300, savefig=True) 36 | 37 | # 鸣潮获取UP5星角色 38 | WW_fig = QuantileFunction( 39 | WW.up_5star_character(7, multi_dist=True), 40 | title='鸣潮获取UP五星角色概率', 41 | item_name='UP五星角色', 42 | text_head='采用推测模型,不保证完全准确\n获取1个UP五星角色最多160抽', 43 | text_tail='@一棵平衡树 '+time.strftime('%Y-%m-%d',time.localtime(time.time())), 44 | max_pull=1150, 45 | line_colors=cm.YlOrBr(np.linspace(0.4, 0.9, 7+1)), # cm.OrAKges(np.linspace(0.5, 0.9, 6+1)), 46 | y_base_gap=25, 47 | y2x_base=2, 48 | is_finite=True) 49 | WW_fig.show_figure(dpi=300, savefig=True) -------------------------------------------------------------------------------- /GGanalysis/games/wuthering_waves/gacha_model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 注意,本模块使用概率模型仅为根据游戏测试阶段推测,不能保证完全准确 3 | 分析文章 https://www.bilibili.com/read/cv34870533/ 4 | ''' 5 | from GGanalysis.distribution_1d import * 6 | from GGanalysis.gacha_layers import * 7 | from GGanalysis.basic_models import * 8 | 9 | __all__ = [ 10 | 'PITY_5STAR', 11 | 'PITY_4STAR', 12 | 'common_5star', 13 | 'common_4star', 14 | 'up_5star_character', 15 | 'up_4star_character', 16 | 'up_4star_specific_character', 17 | 'up_5star_weapon', 18 | 'up_4star_weapon', 19 | 'up_4star_specific_weapon', 20 | ] 21 | 22 | # 鸣潮普通5星保底概率表 23 | PITY_5STAR = np.zeros(80) 24 | PITY_5STAR[1:66] = 0.008 25 | PITY_5STAR[66:71] = np.arange(1, 5+1) * 0.04 + PITY_5STAR[65] 26 | PITY_5STAR[71:76] = np.arange(1, 5+1) * 0.08 + PITY_5STAR[70] 27 | PITY_5STAR[76:80] = np.arange(1, 4+1) * 0.1 + PITY_5STAR[75] 28 | PITY_5STAR[79] = 1 29 | # 鸣潮普通4星保底概率表 30 | PITY_4STAR = np.zeros(11) 31 | PITY_4STAR[1:10] = 0.06 32 | PITY_4STAR[10] = 1 33 | 34 | # 定义获取星级物品的模型 35 | common_5star = PityModel(PITY_5STAR) 36 | common_4star = PityModel(PITY_4STAR) 37 | # 定义鸣潮角色池模型 38 | up_5star_character = DualPityModel(PITY_5STAR, [0, 0.5, 1]) 39 | up_4star_character = DualPityModel(PITY_4STAR, [0, 0.5, 1]) 40 | up_4star_specific_character = DualPityBernoulliModel(PITY_4STAR, [0, 0.5, 1], 1/3) 41 | # 定义鸣潮武器池模型 42 | up_5star_weapon = PityModel(PITY_5STAR) 43 | up_4star_weapon = DualPityModel(PITY_4STAR, [0, 0.5, 1]) 44 | up_4star_specific_weapon = DualPityBernoulliModel(PITY_4STAR, [0, 0.5, 1], 1/3) 45 | 46 | if __name__ == '__main__': 47 | print(1.5*common_5star(1).exp) 48 | print(PITY_5STAR[70]) 49 | print(PITY_5STAR[75]) 50 | ''' 51 | print(PITY_5STAR[70:]) 52 | close_dis = 1 53 | pity_begin = 0 54 | p_raise = 0 55 | for i in range(50, 75+1): 56 | # 枚举开始上升位置 57 | PITY_5STAR = np.zeros(81) 58 | PITY_5STAR[1:i] = 0.008 59 | for j in range(1, 10): 60 | # 枚举每抽上升概率 61 | p_step = j / 100 62 | PITY_5STAR[i:80] = np.arange(1, 80-i+1) * p_step + 0.008 63 | PITY_5STAR[80] = 1 64 | common_5star = PityModel(PITY_5STAR) 65 | p = 1 / common_5star(1).exp 66 | if p > 0.018: 67 | # 达到要求进行记录 68 | if p-0.018 < close_dis: 69 | close_dis = p-0.018 70 | pity_begin = i 71 | p_raise = p_step 72 | print(p, i, p_step, PITY_5STAR[70:81]) 73 | ''' 74 | -------------------------------------------------------------------------------- /GGanalysis/games/wuthering_waves/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.wuthering_waves import PITY_5STAR, PITY_4STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在1.0版本之后五星四星耦合情况下的概率 5 | common_gacha_system = PriorityPitySystem([PITY_5STAR, PITY_4STAR, [0, 1]], remove_pity=True) 6 | print('卡池各星级综合概率', 1/common_gacha_system.get_stationary_p()) 7 | print('四星非10抽保底概率(考虑重置保底)', common_gacha_system.get_type_distribution(type=1)) -------------------------------------------------------------------------------- /GGanalysis/games/zenless_zone_zero/__init__.py: -------------------------------------------------------------------------------- 1 | from .gacha_model import * -------------------------------------------------------------------------------- /GGanalysis/games/zenless_zone_zero/gacha_model.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 注意,本模块使用概率模型仅为根据游戏测试阶段推测,不能保证完全准确 3 | ''' 4 | from GGanalysis.distribution_1d import * 5 | from GGanalysis.gacha_layers import * 6 | from GGanalysis.basic_models import * 7 | 8 | __all__ = [ 9 | 'PITY_5STAR', 10 | 'PITY_4STAR', 11 | 'PITY_W5STAR', 12 | 'PITY_W4STAR', 13 | 'common_5star', 14 | 'common_4star', 15 | 'common_5star_weapon', 16 | 'common_4star_weapon', 17 | 'up_5star_character', 18 | 'up_4star_character', 19 | 'up_4star_specific_character', 20 | 'up_5star_weapon', 21 | 'up_4star_weapon', 22 | 'up_4star_specific_weapon', 23 | ] 24 | 25 | # 绝区零普通5星保底概率表 26 | PITY_5STAR = np.zeros(91) 27 | PITY_5STAR[1:74] = 0.006 28 | PITY_5STAR[74:90] = np.arange(1, 17) * 0.06 + 0.006 29 | PITY_5STAR[90] = 1 30 | # 绝区零普通4星保底概率表 基础概率9.4% 角色池其中角色为7.05% 音擎为2.35% 十抽保底 综合14.4% 31 | # 角色池不触发UP机制时角色占比 1/2 音擎占比 1/2 32 | # 角色池触发UP机制时UP角色占比 1/2 其他角色占比 1/4 音擎占比 1/4 33 | PITY_4STAR = np.zeros(11) 34 | PITY_4STAR[1:10] = 0.094 35 | PITY_4STAR[10] = 1 36 | 37 | # 绝区零音擎5星保底概率表 基础概率1% 综合概率2% 80保底 75%概率单UP 38 | PITY_W5STAR = np.zeros(81) 39 | PITY_W5STAR[1:65] = 0.01 40 | PITY_W5STAR[65:80] = np.arange(1, 16) * 0.06 + 0.01 41 | PITY_W5STAR[80] = 1 42 | # 绝区零音擎4星保底概率表 基础概率15% 其中音擎占13.125% 角色占1.875% 10抽保底 综合概率18% 75%概率UP 43 | # 音擎池不触发UP机制时音擎占比 1/2 角色占比 1/2 44 | # 音擎池触发UP机制时UP音擎占比 3/4 其他音擎占比 1/8 角色占比 1/8 45 | PITY_W4STAR = np.zeros(11) 46 | PITY_W4STAR[1:10] = 0.15 47 | PITY_W4STAR[10] = 1 48 | 49 | # 定义获取星级物品的模型 50 | common_5star = PityModel(PITY_5STAR) 51 | common_4star = PityModel(PITY_4STAR) 52 | # 定义绝区零角色池模型 53 | up_5star_character = DualPityModel(PITY_5STAR, [0, 0.5, 1]) 54 | up_4star_character = DualPityModel(PITY_4STAR, [0, 0.5, 1]) 55 | up_4star_specific_character = DualPityBernoulliModel(PITY_4STAR, [0, 0.5, 1], 1/2) 56 | # 定义绝区零武器池模型 57 | common_5star_weapon = PityModel(PITY_W5STAR) 58 | common_4star_weapon = PityModel(PITY_W4STAR) 59 | up_5star_weapon = DualPityModel(PITY_W5STAR, [0, 0.75, 1]) 60 | up_4star_weapon = DualPityModel(PITY_W4STAR, [0, 0.75, 1]) 61 | up_4star_specific_weapon = DualPityBernoulliModel(PITY_W4STAR, [0, 0.75, 1], 1/2) 62 | # 定义绝区零邦布池模型 63 | bangboo_5star = PityModel(PITY_5STAR) 64 | bangboo_4star = PityModel(PITY_4STAR) 65 | 66 | if __name__ == '__main__': 67 | print(common_5star(1).exp) 68 | print(common_5star_weapon(1).exp) 69 | print(common_4star(1).exp) 70 | print(common_4star_weapon(1).exp) 71 | print(up_5star_weapon(1).exp) 72 | ''' 73 | close_dis = 1 74 | pity_begin = 0 75 | p_raise = 0 76 | for i in range(60, 75+1): 77 | # 枚举开始上升位置 78 | PITY_5STAR = np.zeros(81) 79 | PITY_5STAR[1:i] = 0.01 80 | for j in range(5, 10): 81 | # 枚举每抽上升概率 82 | p_step = j / 100 83 | PITY_5STAR[i:80] = np.arange(1, 80-i+1) * p_step + 0.01 84 | PITY_5STAR[80] = 1 85 | common_5star = PityModel(PITY_5STAR) 86 | p = 1 / common_5star(1).exp 87 | if p > 0.02: 88 | # 达到要求进行记录 89 | if p-0.02 < close_dis: 90 | close_dis = p-0.02 91 | pity_begin = i 92 | p_raise = p_step 93 | print(p, i, p_step, PITY_5STAR[70:81]) 94 | ''' 95 | -------------------------------------------------------------------------------- /GGanalysis/games/zenless_zone_zero/stationary_p.py: -------------------------------------------------------------------------------- 1 | from GGanalysis.games.zenless_zone_zero import PITY_5STAR, PITY_4STAR, PITY_W5STAR, PITY_W4STAR 2 | from GGanalysis.markov_method import PriorityPitySystem 3 | 4 | # 调用预置工具计算在五星四星耦合情况下的概率 5 | common_gacha_system = PriorityPitySystem([PITY_5STAR, PITY_4STAR, [0, 1]], remove_pity=True) 6 | print('常驻及角色池概率', common_gacha_system.get_stationary_p()) 7 | print('常驻及角色池分布', common_gacha_system.get_type_distribution(1)) 8 | 9 | weapon_gacha_system = PriorityPitySystem([PITY_W5STAR, PITY_W4STAR, [0, 1]], remove_pity=True) 10 | print('音擎及邦布池概率', weapon_gacha_system.get_stationary_p()) 11 | print('音擎及邦布池分布', weapon_gacha_system.get_type_distribution(1)) -------------------------------------------------------------------------------- /GGanalysis/markov_method.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.special import comb 3 | from GGanalysis.distribution_1d import * 4 | from GGanalysis.basic_models import PityModel 5 | 6 | ''' 7 | 基于解马尔科夫链平稳分布的分析工具 8 | ''' 9 | def table2matrix(state_num, state_trans): 10 | ''' 11 | 将邻接表变为邻接矩阵 12 | 13 | - ``state_num`` : 状态数量 14 | - ``state_trans`` :邻接表 15 | 16 | 根据 state_num 和 state_trans 构造矩阵示例 17 | 18 | .. code:: Python 19 | 20 | # Epitomized Path & Fate Points 21 | state_num = {'get':0, 'fate1UP':1, 'fate1':2, 'fate2':3} 22 | state_trans = [ 23 | ['get', 'get', 0.375], 24 | ['get', 'fate1UP', 0.375], 25 | ['get', 'fate1', 0.25], 26 | ['fate1UP', 'get', 0.375], 27 | ['fate1UP', 'fate2', 1-0.375], 28 | ['fate1', 'get', 0.5], 29 | ['fate1', 'fate2', 0.5], 30 | ['fate2', 'get', 1] 31 | ] 32 | matrix = table2matrix(state_num, state_trans) 33 | ''' 34 | M = np.zeros((len(state_num), len(state_num))) 35 | for name_a, name_b, p in state_trans: 36 | a = state_num[name_a] 37 | b = state_num[name_b] 38 | M[b][a] = p 39 | # 检查每个节点出口概率和是否为1, 但这个并不是特别广义 40 | ''' 41 | for index, element in enumerate(np.sum(M, axis=0)): 42 | if element != 1: 43 | raise Warning('The sum of probabilities is not 1 at position '+str(index)) 44 | ''' 45 | return M 46 | 47 | def calc_stationary_distribution(M): 48 | ''' 49 | 计算转移矩阵对应平稳分布 50 | 51 | 转移矩阵如下,平稳分布为列向量 52 | 53 | .. code-block:: none 54 | 55 | |1 0.5| |x| 56 | |0 0.5| |y| 57 | 58 | x = x + 0.5y 59 | y = 0.5y 60 | 所得平稳分布为转移矩阵特征值1对应的特征向量 61 | ''' 62 | matrix_shape = np.shape(M) 63 | if matrix_shape[0] == matrix_shape[1]: 64 | pass 65 | else: 66 | print("平稳分布计算错误:输入应该为方阵") 67 | return 68 | # 减去对角阵 69 | C = M - np.identity(matrix_shape[0]) 70 | # 末行设置为1 71 | C[matrix_shape[0]-1] = 1 72 | # 设置向量 73 | X = np.zeros(matrix_shape[0], dtype=float) 74 | X[matrix_shape[0]-1] = 1 75 | # 解线性方程求解 76 | ans = np.linalg.solve(C, X) 77 | return ans 78 | 79 | class PriorityPitySystem(object): 80 | """ 81 | 不同道具按照优先级排序的保底系统 82 | 若道具为固定概率p,则传入列表填为 [0, p] 83 | """ 84 | def __init__(self, item_p_list: list, extra_state = 1, remove_pity = False) -> None: 85 | # TODO extra_state 设置为0会产生问题,需要纠正 (但现在看好像没问题来着) 86 | self.item_p_list = item_p_list # 保底参数列表 按高优先级到低优先级排序 87 | self.item_types = len(item_p_list) # 一共有多少种道具 88 | self.remove_pity = remove_pity 89 | self.extra_state = extra_state # 对低优先级保底道具的情况额外延长几个状态,默认为1 90 | 91 | self.pity_state_list = [] # 记录每种道具保留几个状态 92 | self.pity_pos_max = [] # 记录每种道具没有干扰时原始保底位置 93 | for pity_p in item_p_list: 94 | self.pity_state_list.append(len(pity_p)+extra_state-1) 95 | self.pity_pos_max.append(len(pity_p)-1) 96 | 97 | self.max_state = 1 # 最多有多少种状态 98 | for pity_state in self.pity_state_list: 99 | self.max_state = self.max_state * pity_state 100 | 101 | # 计算转移矩阵并获得平稳分布 102 | self.transfer_matrix = self.get_transfer_matrix() 103 | self.stationary_distribution = calc_stationary_distribution(self.transfer_matrix) 104 | 105 | def item_pity_p(self, item_type, p_pos): 106 | # 获得不考虑相互影响情况下的保底概率 107 | return self.item_p_list[item_type][min(p_pos, self.pity_pos_max[item_type])] 108 | 109 | def get_state(self, state_num) -> list: 110 | """ 111 | 根据状态编号获得保底情况 112 | """ 113 | pity_state = [] 114 | for i in self.pity_state_list[::-1]: 115 | pity_state.append(state_num % i) 116 | state_num = state_num // i 117 | return pity_state[::-1] 118 | 119 | def get_number(self, pity_state) -> int: 120 | """ 121 | 根据保底情况获得状态编号 122 | """ 123 | number = 0 124 | last = 1 125 | for i, s in zip(self.pity_state_list[::-1], pity_state[::-1]): 126 | number += s * last 127 | last *= i 128 | return number 129 | 130 | def get_next_state(self, pity_state, get_item=None) -> list: 131 | """ 132 | 返回下一个状态 133 | 134 | pity_state 为当前状态 get_item 为当前获取的道具,若为 None 则表示没有获得数据 135 | """ 136 | next_state = [] 137 | for i in range(self.item_types): 138 | if get_item == i: # 获得了此类物品 139 | next_state.append(0) 140 | else: # 没有获得此类物品 141 | # 若有高优先级清除低优先级保底的情况 142 | if self.remove_pity and get_item is not None: 143 | # 本次为低优先级物品 144 | if i > get_item: 145 | next_state.append(0) 146 | continue 147 | # 没有高优先级清除低优先级保底的情况 148 | next_state.append(min(self.pity_state_list[i]-1, pity_state[i]+1)) 149 | return next_state 150 | 151 | def get_transfer_matrix(self) -> np.ndarray: 152 | """ 153 | 根据当前的设置生成转移矩阵 154 | """ 155 | M = np.zeros((self.max_state, self.max_state)) # 状态编号从0开始,0也是其中一种状态 156 | 157 | for i in range(self.max_state): 158 | left_p = 1 159 | current_state = self.get_state(i) 160 | # 本次获得了道具 161 | for item_type, p_pos in zip(range(self.item_types), current_state): 162 | next_state = self.get_next_state(current_state, item_type) 163 | transfer_p = min(left_p, self.item_pity_p(item_type, p_pos+1)) 164 | M[self.get_number(next_state)][i] = transfer_p 165 | left_p = left_p - transfer_p 166 | # 本次没有获得任何道具 167 | next_state = self.get_next_state(current_state, None) 168 | M[self.get_number(next_state)][i] = left_p 169 | return M 170 | 171 | def get_stationary_p(self) -> list: 172 | """ 173 | 以列表形式返回每种道具的综合概率 174 | """ 175 | stationary_p = np.zeros(len(self.item_p_list)) 176 | for i in range(self.max_state): 177 | current_state = self.get_state(i) 178 | # 将当前状态的概率累加到对应物品上 179 | for j, item_state in enumerate(current_state): 180 | if item_state == 0: 181 | stationary_p[j] += self.stationary_distribution[i] 182 | # 高优先级优先,不计入低优先级物品 183 | break 184 | return stationary_p 185 | 186 | def get_type_distribution(self, type) -> np.ndarray: 187 | """ 188 | 获取对于某一类道具花费抽数的分布(平稳分布的情况) 189 | """ 190 | ans = np.zeros(self.pity_state_list[type]+1) 191 | # print('shape', ans.shape) 192 | for i in range(self.max_state): 193 | left_p = 1 194 | current_state = self.get_state(i) 195 | for item_type, p_pos in zip(range(type), current_state[:type]): 196 | left_p -= self.item_pity_p(item_type, p_pos+1) 197 | # 转移概率 198 | transfer_p = min(max(0, left_p), self.item_pity_p(type, current_state[type]+1)) 199 | next_pos = min(self.pity_state_list[type], current_state[type]+1) 200 | ans[next_pos] += self.stationary_distribution[i] * transfer_p 201 | return ans/sum(ans) 202 | 203 | def multi_item_rarity(pity_p: list, once_pull_times: int, is_complete=True): 204 | ''' 205 | 计算连抽情况下获得多个道具的概率 206 | 仅仅适用于保底抽卡模型 207 | 208 | 采用两阶段计算,先得出一直连抽后的剩余保底情况平稳分布,再在平稳分布的基础上通过枚举可能情况算出概率 209 | ''' 210 | # 计算抽 k 抽都没有道具的概率 211 | P_m = np.zeros(once_pull_times+1, dtype=float) 212 | P_m[0] = 1 213 | for i in range(1, once_pull_times+1): 214 | if i < len(pity_p): 215 | P_m[i] = P_m[i-1] * (1-pity_p[i]) 216 | else: 217 | P_m[i] = 0 218 | # 设置道具获取模型 219 | item_model = PityModel(pity_p) 220 | # 计算连抽后剩余保底 221 | item_left_model = PriorityPitySystem([pity_p], extra_state=0) 222 | stationary_left = item_left_model.stationary_distribution 223 | # 使用广义方法计算连抽得多个道具概率 224 | ans = np.zeros(once_pull_times+1, dtype=np.double) 225 | # 枚举出多少个道具 226 | for i in range(1, once_pull_times+1): 227 | # 枚举之前垫了多少抽 228 | for j in range(len(pity_p)-1): 229 | dist = item_model(i, item_pity=j) 230 | for k in range(1, len(dist.dist[:once_pull_times+1])): 231 | ans[i] += stationary_left[j] * dist.dist[k] * P_m[once_pull_times-k] 232 | ans[0] = 1 - sum(ans[1:]) 233 | return ans -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 一棵平衡树 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=../../ggdoc 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # GGanalysis 工具包文档 2 | 3 | 使用主分支下 `docs` 内文档构建 4 | 5 | ### 配置方法 6 | 7 | ``` shell 8 | pip install Sphinx==6.1.3 9 | pip install furo==2023.3.27 10 | pip install sphinx-copybutton==0.5.1 11 | ``` 12 | 13 | ### Windows下生成 html 文件 14 | 15 | 在主分支下 `docs` 内文档使用如下命令 16 | 17 | ``` shell 18 | ./make.bat html 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==6.1.3 2 | furo==2023.3.27 3 | sphinx-copybutton==0.5.1 -------------------------------------------------------------------------------- /docs/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | div.toctree-wrapper > p.caption { 2 | display: none; 3 | } -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | sys.path.insert(0, os.path.abspath('../../GGanalysis')) 4 | 5 | # Configuration file for the Sphinx documentation builder. 6 | # 7 | # For the full list of built-in configuration values, see the documentation: 8 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 9 | 10 | # -- Project information ----------------------------------------------------- 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 12 | 13 | project = 'GGanalysis' 14 | copyright = '2023, OneBST' 15 | author = 'OneBST' 16 | release = '0.3.0' 17 | 18 | # -- General configuration --------------------------------------------------- 19 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 20 | 21 | extensions = [ 22 | 'sphinx.ext.autodoc', 23 | 'sphinx.ext.autosummary', 24 | 'sphinx.ext.githubpages', 25 | 'sphinx.ext.todo', 26 | 'sphinx.ext.mathjax', 27 | 'sphinx.ext.coverage', 28 | 'sphinx.ext.napoleon', 29 | 'sphinx_copybutton', 30 | ] 31 | 32 | templates_path = ['_templates'] 33 | exclude_patterns = [] 34 | 35 | language = 'zh_CN' 36 | html_search_language = 'zh' 37 | # Automatically generate stub pages when using the .. autosummary directive 38 | autosummary_generate = True 39 | 40 | # -- Options for HTML output ------------------------------------------------- 41 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 42 | 43 | html_theme = "furo" # pip install furo 44 | html_static_path = ["_static"] 45 | html_css_files = ['custom.css'] 46 | html_title = "GGanalysis" 47 | html_favicon = '_static/favicon.svg' # 网页缩略图 48 | html_theme_options = { 49 | "sidebar_hide_name": True, 50 | "light_logo": "light_logo.svg", 51 | "dark_logo": "dark_logo.svg", 52 | "source_repository": "https://github.com/OneBST/GGanalysis", 53 | "source_branch": "develop", 54 | "source_directory": "docs/source", 55 | "footer_icons": [ 56 | { 57 | "name": "GitHub", 58 | "url": "https://github.com/OneBST/GGanalysis", 59 | "html": """ 60 | 61 | 62 | 63 | """, 64 | "class": "", 65 | }, 66 | ], 67 | } -------------------------------------------------------------------------------- /docs/source/games/alchemy_stars/index.rst: -------------------------------------------------------------------------------- 1 | 少女前线2:追放 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ 5 | -------------------------------------------------------------------------------- /docs/source/games/arknights/index.rst: -------------------------------------------------------------------------------- 1 | .. _arknights_gacha_model: 2 | 3 | 明日方舟抽卡模型 4 | ======================== 5 | 6 | GGanalysis 使用基本的抽卡模板模型结合 `明日方舟抽卡系统参数 `_ 定义了一系列可以直接取用的抽卡模型。需要注意的是明日方舟的抽卡系统模型的确定程度并没有很高,使用时需要注意。 7 | 8 | 此外,还针对性编写了如下模板模型: 9 | 10 | 适用于计算 `定向选调 `_ 时获得特定UP六星干员的模型 11 | :class:`~GGanalysis.games.arknights.AKDirectionalModel` 12 | 13 | 适用于计算通过 `统计数据 `_ 发现的类型硬保底的模型 14 | :class:`~GGanalysis.games.arknights.AKHardPityModel` 15 | 16 | 适用于计算集齐多种六星的模型(不考虑300井、定向选调及类型硬保底机制) 17 | :class:`~GGanalysis.games.arknights.AK_Limit_Model` 18 | 19 | .. 本节部分内容自一个资深的烧饼编写文档修改而来,MilkWind 增写内容 20 | 21 | 参数意义 22 | ------------------------ 23 | 24 | - ``item_num`` 需求道具个数,由于 sphinx autodoc 的 `bug `_ 在下面没有显示 25 | 26 | - ``multi_dist`` 是否以列表返回获取 1-item_num 个道具的所有分布列,默认为False 27 | 28 | - ``item_pity`` 道具保底状态,通俗的叫法为水位、垫抽,默认为0 29 | 30 | - ``type_pity`` 定向选调的类型保底状态,默认为0,即该机制尚未开始触发;若为其它数,如20,那么就意味着定向选调机制已经垫了20抽,还有130抽就位于该保底机制的触发范围了(算上偏移量,实际为131抽) 31 | 32 | - ``calc_pull`` 采用伯努利模型时最高计算的抽数,高于此不计算(仅五星采用伯努利模型) 33 | 34 | 六星模型 35 | ------------------------ 36 | 37 | **获取任意六星干员** 38 | 39 | .. automethod:: GGanalysis.games.arknights.gacha_model.common_6star 40 | 41 | .. code:: python 42 | 43 | import GGanalysis.games.arknights as AK 44 | dist = AK.common_6star(item_num=1) 45 | print('抽到六星的期望抽数为:{}'.format(dist.exp)) # 34.59455493520977 46 | 47 | **无定向选调获取标准寻访-单UP六星干员** 48 | 49 | .. automethod:: GGanalysis.games.arknights.gacha_model.single_up_6star_old 50 | 51 | **有定向选调获取标准寻访-单UP六星干员** 52 | 53 | .. automethod:: GGanalysis.games.arknights.gacha_model.single_up_6star 54 | 55 | .. code:: python 56 | 57 | import GGanalysis.games.arknights as AK 58 | dist = AK.single_up_6star(item_num=1, item_pity=0, type_pity=0) 59 | print('4.6寻访机制更新后,无水位时抽到单up六星的期望抽数为:{}'.format(dist.exp)) 60 | 61 | .. container:: output stream stdout 62 | 63 | :: 64 | 65 | 4.6寻访机制更新后,无水位时抽到单up六星的期望抽数为:66.16056206529494 66 | 67 | **无类型硬保底轮换池获取特定六星干员** 68 | 69 | .. automethod:: GGanalysis.games.arknights.gacha_model.dual_up_specific_6star_old 70 | 71 | **有类型硬保底时轮换池获取特定六星干员** 72 | 73 | .. automethod:: GGanalysis.games.arknights.gacha_model.dual_up_specific_6star 74 | 75 | .. code:: python 76 | 77 | import GGanalysis.games.arknights as AK 78 | dist = AK.dual_up_specific_6star(item_num=1) 79 | print('准备100抽,从轮换池捞出玛恩纳的概率只有:{}%'.format(sum(dist[:100+1]) * 100)) 80 | 81 | .. container:: output stream stdout 82 | 83 | :: 84 | 85 | 准备100抽,从轮换池捞出玛恩纳的概率只有:49.60442859476116% 86 | 87 | **双UP限定池获取特定六星干员** 88 | 89 | .. automethod:: GGanalysis.games.arknights.gacha_model.limited_up_6star 90 | 91 | 92 | 需要注意的是,此模型返回的结果是不考虑井的分布。如需考虑井需要自行进行一定后处理。 93 | 94 | 95 | .. code:: python 96 | 97 | import GGanalysis.games.arknights as AK 98 | dist = AK.limited_up_6star(item_num=5) 99 | print('一井满潜限定的概率:{}%'.format(sum(dist_4[:300+1]) * 100)) 100 | 101 | .. container:: output stream stdout 102 | 103 | :: 104 | 105 | 一井满潜限定的概率:14.881994954229667% 106 | 107 | **双UP限定池集齐两种UP六星干员** 108 | 109 | .. automethod:: GGanalysis.games.arknights.gacha_model.limited_both_up_6star 110 | 111 | .. code:: python 112 | 113 | import GGanalysis.games.arknights as AK 114 | dist = AK.limited_both_up_6star() 115 | print('全六党吃井概率:{}%'.format((1-sum(dist[:300+1])) * 100)) 116 | 117 | .. container:: output stream stdout 118 | 119 | :: 120 | 121 | 全六党吃井概率:7.130522684168872% 122 | 123 | 五星模型 124 | ------------------------ 125 | 126 | .. attention:: 127 | 128 | 明日方舟五星干员实际上有概率递增的星级保底机制,但其保底进度会被六星重置。这里对五星模型采用了近似,认为其是一个概率为考虑了概率递增的伯努利模型。另外,此处提供的五星模型也没有考虑类型保底。 129 | 130 | 此外明日方舟五星模型没有采用 :class:`~GGanalysis.BernoulliLayer` 构建模型,而是直接采用了 :class:`~GGanalysis.BernoulliGachaModel` ,当设置 ``calc_pull`` 太低时,返回的分布概率和可能距离 1 有相当大差距,需要适当设高。 131 | 132 | **获取任意五星干员** 133 | 134 | .. automethod:: GGanalysis.games.arknights.gacha_model.common_5star 135 | 136 | **获取单UP五星干员** 137 | 138 | .. automethod:: GGanalysis.games.arknights.gacha_model.single_up_specific_5star 139 | 140 | **获取双UP中特定五星干员** 141 | 142 | .. automethod:: GGanalysis.games.arknights.gacha_model.dual_up_specific_5star 143 | 144 | **获取三UP中特定五星干员** 145 | 146 | .. automethod:: GGanalysis.games.arknights.gacha_model.triple_up_specific_5star 147 | 148 | 149 | 自定义抽卡模型例子 150 | ------------------------ 151 | 152 | .. attention:: 153 | 154 | ``AKDirectionalModel`` 可以为某个 ``FiniteDist`` 类型的,无保底类型的分布载入定向选调机制。 155 | 156 | ``AKHardPityModel`` 可以为某个 ``FiniteDist`` 类型的,无保底类型的分布载入类型硬保底机制。 157 | 158 | ``AK_Limit_Model`` 未加入新增的定向选调机制,其使用的 `CouponCollectorLayer` 不考虑集齐多套的需求。这个模型接下来可能重写,如果想要在其他地方引用的话可以先临时复制代码出来本地使用,或是将 `AK_Limit_Model` 加入__all__公开列表进行调用。 159 | 160 | **联合行动池集齐三种UP六星干员** 161 | 162 | .. code:: python 163 | 164 | import GGanalysis.games.arknights as AK 165 | triple_up_specific_6star = AK.AK_Limit_Model(AK.PITY_6STAR, 1, total_item_types=3, collect_item=3) 166 | dist = triple_up_specific_6star(item_pity=5) # (默认)期望集齐一轮,此前垫了5抽 167 | print('期望抽数为:{}'.format(dist.exp)) # 期望抽数为:188.63258247595024 168 | print('方差为:{}'.format(dist.var)) # 方差为:10416.175324956945 169 | print('100抽以内达成目标的概率为:{}%'.format(sum(dist[:100+1]) * 100)) # 100抽以内达成目标的概率为:16.390307170816875% 170 | 171 | **定向寻访池获取特定六星干员** 172 | 173 | .. code:: python 174 | 175 | import GGanalysis as gg 176 | import GGanalysis.games.arknights as AK 177 | # 六星100%概率,UP三个,故抽到目标UP六星的概率为1/3 178 | triple_up_specific_6star = gg.PityBernoulliModel(AK.PITY_6STAR, 1 / 3) # 尚未证实定向寻访是否存在类型硬保底机制,仅使用保底伯努利模型 179 | dist = triple_up_specific_6star(2) # 在定向寻访池期望抽到目标六星干员两次,此前没有垫抽 180 | print('期望抽数为:{}'.format(dist.exp)) # 期望抽数为:207.56732961125866 181 | 182 | **双UP限定池获取特定权值提升的非UP六星干员** 183 | 184 | 非UP六星干员在六星中占比采用下式计算 185 | 186 | .. math:: p = 0.3\frac{5}{5 * \text{secondary up number} + \text{others number}} 187 | 188 | 其中 ``secondary up number`` 为权值提升的非UP六星干员数量, ``others number`` 为除了主要UP干员和权值提升的非UP六星干员,其他准许获取的六星干员的数量。 189 | 190 | .. code:: python 191 | 192 | import GGanalysis as gg 193 | others = 71 # 假设除了主要UP干员和权值提升的非UP六星干员外,其他准许获取的六星干员的数量为71 194 | triple_second_up_specific_6star = gg.PityBernoulliModel(AK.PITY_6STAR, 0.3 / (5 * 3 + others) * 5) # 在当前卡池内,权值提升的非UP六星干员数量一般为3 195 | success_count = 3 # 期望抽到某个权值提升的非UP六星干员三次 196 | dist = triple_second_up_specific_6star(success_count, True) # `multi_dist` 为True表示以列表形式返回分布 197 | for i in range(1, success_count + 1): 198 | print(f"抽到第{i}个目标干员~期望抽数:{round(dist[i].exp, 2)},方差:{round(dist[i].var, 2)}") # 结果保留两位小数 199 | 200 | # 抽到第1个目标干员~期望抽数:1983.42,方差:3890458.19 201 | # 抽到第2个目标干员~期望抽数:3966.84,方差:7780916.38 202 | # 抽到第3个目标干员~期望抽数:5950.26,方差:11671374.57 203 | 204 | # 不太建议计算非主要UP的干员的数据,分布会很长 205 | 206 | **标准寻访-单UP池中集齐两种UP五星干员** 207 | 208 | .. code:: python 209 | 210 | import GGanalysis.games.arknights as AK 211 | both_up_5star = AK.AK_Limit_Model(AK.PITY_5STAR, 0.5, total_item_types=2, collect_item=2) 212 | dist = both_up_5star() # 期望在轮换单UP池中抽到两个UP的五星干员,此前没有垫抽 213 | print('期望抽数为:{}'.format(dist.exp)) # 期望抽数为:63.03402819816313 214 | 215 | **自定义带有类型硬保底的模型** 216 | 217 | .. code:: python 218 | 219 | # 添加定向选调前已知存在类型硬保底的卡池为标准寻访中的单UP和双UP池,其它卡池暂无证据表明存在此机制,此处仅为演示如何定义此类模型,不能当做机制参考 220 | import GGanalysis as gg 221 | import GGanalysis.games.arknights as AK 222 | # 假设定向寻访池存在类型硬保底机制 223 | 224 | # 六星100%概率,UP三个,故抽到目标UP六星的概率为1/3 225 | triple_up_specific_6star_without_hard_pity = gg.PityBernoulliModel(AK.PITY_6STAR, 1 / 3) # 没有硬保底 226 | triple_up_specific_6star_has_hard_pity = AK.AKHardPityModel(triple_up_specific_6star_without_hard_pity(1), AK.p2dist(AK.PITY_6STAR), type_pity_gap=200, item_types=3, up_rate=1, type_pull_shift=1) # 载入硬保底 227 | dist = triple_up_specific_6star_has_hard_pity(2) # 在定向寻访池期望抽到目标六星干员两次,此前没有垫抽 228 | print('期望抽数为:{}'.format(dist.exp)) # 期望抽数为:207.0218117279958 -------------------------------------------------------------------------------- /docs/source/games/azur_lane/index.rst: -------------------------------------------------------------------------------- 1 | 碧蓝航线抽卡模型 2 | ======================== 3 | 4 | .. attention:: 5 | 6 | GGanalysis 提供的预定义模型没有考虑 200 抽天井的情况,如需考虑此情况请参照此 :ref:`示例代码 ` 。 7 | 碧蓝航线的预定义抽卡模型按卡池 **彩 金 紫** 道具数量进行模型命名,例如1彩2金3紫模型命名为 ``model_1_2_3``,此例中预定义的彩、金、紫道具名称分别为 ``UR1`` ``SSR1`` ``SSR2`` ``SR1`` ``SR2`` ``SR3``。 8 | 使用时输入初始道具收集状态和目标道具收集状态,模型以 ``FiniteDist`` 类型返回所需抽数分布。 9 | 10 | 参数意义 11 | ------------------------ 12 | 13 | - ``init_item`` 初始道具收集状态,一个包含了已经拥有哪些道具的字符串列表,由于 sphinx autodoc 的 `bug `_ 在下面没有显示 14 | 15 | - ``target_item`` 目标道具收集状态,一个包含了目标要收集哪些道具的字符串列表 16 | 17 | 预定义模型 18 | ------------------------ 19 | 20 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_1_1_3 21 | 22 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_0_3_2 23 | 24 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_0_2_3 25 | 26 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_0_2_2 27 | 28 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_0_2_1 29 | 30 | .. automethod:: GGanalysis.games.azur_lane.gacha_model.model_0_1_1 31 | 32 | .. code:: python 33 | 34 | import GGanalysis.games.azur_lane as AL 35 | # 碧蓝航线2金3紫卡池在已有1金0紫的情况下集齐剩余1金及特定2紫的情况 36 | dist = AL.model_0_2_3(init_item=['SSR1'], target_item=['SSR1', 'SSR2', 'SR1', 'SR2']) 37 | print('期望为', dist.exp, '方差为', dist.var, '分布为', dist.dist) 38 | 39 | .. _azur_lane_hard_pity_example: 40 | 41 | 处理天井示例代码 42 | ------------------------ 43 | 44 | .. code:: python 45 | 46 | import GGanalysis as gg 47 | import GGanalysis.games.azur_lane as AL 48 | # 对于 1_1_3 带200天井的情况,需要单独处理 49 | dist_0 = AL.model_1_1_3(target_item=['UR1', 'SSR1']) # 未触发200天井时 50 | dist_1 = AL.model_1_1_3(target_item=['SSR1']) # 触发200天井时 51 | cdf_0 = gg.dist2cdf(dist_0) 52 | cdf_1 = gg.dist2cdf(dist_1) 53 | cdf_0 = cdf_0[:min(len(cdf_0), len(cdf_1))] 54 | cdf_1 = cdf_1[:min(len(cdf_0), len(cdf_1))] 55 | cdf_0[200:] = cdf_1[200:] 56 | # 此时 cdf_0 就是含天井的累积概率密度函数 57 | 58 | -------------------------------------------------------------------------------- /docs/source/games/blue_archive/index.rst: -------------------------------------------------------------------------------- 1 | 蔚蓝档案 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ 5 | -------------------------------------------------------------------------------- /docs/source/games/genshin_impact/artifact_models.rst: -------------------------------------------------------------------------------- 1 | 原神圣遗物模型 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ -------------------------------------------------------------------------------- /docs/source/games/genshin_impact/gacha_models.rst: -------------------------------------------------------------------------------- 1 | .. _genshin_gacha_model: 2 | 3 | 原神抽卡模型 4 | ======================== 5 | 6 | GGanalysis 使用基本的抽卡模板模型结合 `原神抽卡系统参数 `_ 定义了一系列可以直接取用的抽卡模型。 7 | 8 | 此外,还针对性编写了如下模板模型: 9 | 10 | 适用于计算5.0版本前武器活动祈愿定轨时获取道具问题的模型 11 | :class:`~GGanalysis.games.genshin_impact.ClassicGenshin5starEPWeaponModel` 12 | 13 | 适用于计算在活动祈愿中获得常驻祈愿五星/四星道具的模型 14 | :class:`~GGanalysis.games.genshin_impact.GenshinCommon5starInUPpoolModel` 15 | 16 | .. attention:: 17 | 18 | 原神的四星保底不会被五星重置,但与五星耦合时仍会在综合概率上产生细微的影响。此处的模型没有考虑四星和五星的耦合。 19 | 20 | 原神常驻祈愿中具有“平稳机制”,即角色和武器两种类型的保底,GGanalysis 包没有提供这类模型,有需要可以使用 `GGanalysislib 包 `_ 。角色活动祈愿及武器活动祈愿中也有此类机制,由于其在“UP机制”后生效,对于四星UP道具抽取可忽略。 21 | 22 | 参数意义 23 | ------------------------ 24 | 25 | - ``item_num`` 需求物品个数,由于 sphinx autodoc 的 `bug `_ 在下面没有显示 26 | 27 | - ``multi_dist`` 是否以列表返回获取 1-item_num 个物品的所有分布列 28 | 29 | - ``item_pity`` 道具保底状态,通俗的叫法为水位、垫抽 30 | 31 | - ``up_pity`` UP道具保底状态,设为 1 即为玩家所说的大保底 32 | 33 | - ``cr_pity`` 「捕获明光」保底状态 34 | 35 | 基本模型 36 | ------------------------ 37 | 38 | **角色活动祈愿及常驻祈愿获得五星道具的模型** 39 | 40 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.common_5star 41 | 42 | **角色活动祈愿及常驻祈愿获得四星道具的模型** 43 | 44 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.common_4star 45 | 46 | 角色活动祈愿模型 47 | ------------------------ 48 | 49 | **角色活动祈愿5.0版本后获得UP五星角色的模型** 50 | 51 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_5star_character 52 | 53 | **角色活动祈愿5.0版本前获得UP五星角色的模型** 54 | 55 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.classic_up_5star_character 56 | 57 | **角色活动祈愿获得任意UP四星角色的模型** 58 | 59 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_4star_character 60 | 61 | **角色活动祈愿获得特定UP四星角色的模型** 62 | 63 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_4star_specific_character 64 | 65 | .. code:: python 66 | 67 | import GGanalysis.games.genshin_impact as GI 68 | # 原神角色池的计算 69 | print('角色池在垫了20抽,有大保底,已经连歪两次的情况下抽3个UP五星抽数的分布') 70 | dist_c = GI.up_5star_character(item_num=3, item_pity=20, up_pity=1, cr_pity=2) 71 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 72 | 73 | 武器活动祈愿模型 74 | ------------------------ 75 | 76 | **武器活动祈愿获得五星武器的模型** 77 | 78 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.common_5star_weapon 79 | 80 | **武器活动祈愿获得UP五星武器的模型** 81 | 82 | 注意此模型建模的是获得任意一个UP五星武器即满足要求的情况 83 | 84 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_5star_weapon 85 | 86 | **武器活动祈愿无定轨情况下获得特定UP五星武器的模型** 87 | 88 | 注意此模型建模的是2.0前无定轨情况下获得特定UP五星武器的情况 89 | 90 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.classic_up_5star_specific_weapon 91 | 92 | **武器活动祈愿5.0版本后定轨情况下获得特定UP五星武器的模型** 93 | 94 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_5star_ep_weapon 95 | 96 | **武器活动祈愿获得四星武器的模型** 97 | 98 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.common_4star_weapon 99 | 100 | **武器活动祈愿获得UP四星武器的模型** 101 | 102 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_4star_weapon 103 | 104 | **武器活动祈愿获得特定UP四星武器的模型** 105 | 106 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.up_4star_specific_weapon 107 | 108 | .. code:: python 109 | 110 | import GGanalysis.games.genshin_impact as GI 111 | print('武器池池在垫了30抽,有大保底,命定值为1的情况下抽1个UP五星抽数的分布') 112 | dist_w = GI.up_5star_ep_weapon(item_num=1, item_pity=30, up_pity=1, fate_point=1) 113 | print('期望为', dist_w.exp, '方差为', dist_w.var, '分布为', dist_w.dist) 114 | 115 | 其它模型 116 | ------------------------ 117 | 118 | **5.0前从角色活动祈愿中获取位于常驻祈愿的特定五星角色的模型** 119 | 120 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.classic_stander_5star_character_in_up 121 | 122 | **5.0前从武器活动祈愿中获取位于常驻祈愿的特定五星武器的模型** 123 | 124 | .. automethod:: GGanalysis.games.genshin_impact.gacha_model.classic_stander_5star_weapon_in_up 125 | 126 | 其它使用示例 127 | ------------------------ 128 | 129 | .. code:: python 130 | 131 | # 联合角色池和武器池 132 | print('在前述条件下抽3个UP五星角色,1个特定UP武器所需抽数分布') 133 | dist_c_w = dist_c * dist_w 134 | print('期望为', dist_c_w.exp, '方差为', dist_c_w.var, '分布为', dist_c_w.dist) 135 | 136 | # 对比玩家运气 137 | dist_c = GI.up_5star_character(item_num=10) 138 | dist_w = GI.up_5star_ep_weapon(item_num=3) 139 | print('在同样抽了10个UP五星角色,3个特定UP五星武器的玩家中,仅花费1000抽的玩家排名前', str(round(100*sum((dist_c * dist_w)[:1001]), 2))+'%') -------------------------------------------------------------------------------- /docs/source/games/genshin_impact/index.rst: -------------------------------------------------------------------------------- 1 | 原神 2 | ======================== 3 | .. toctree:: 4 | :maxdepth: 1 5 | :caption: 目录 6 | 7 | gacha_models 8 | artifact_models 9 | -------------------------------------------------------------------------------- /docs/source/games/girls_frontline2_exilium/index.rst: -------------------------------------------------------------------------------- 1 | 白夜极光 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ 5 | -------------------------------------------------------------------------------- /docs/source/games/honkai_star_rail/gacha_models.rst: -------------------------------------------------------------------------------- 1 | 崩坏:星穹铁道抽卡模型 2 | ======================== 3 | 4 | GGanalysis 使用基本的抽卡模板模型结合 `基于1500万抽数据统计的崩坏:星穹铁道抽卡系统 `_ 定义了一系列可以直接取用的抽卡模型。 5 | 6 | .. attention:: 7 | 8 | 崩坏:星穹铁道的四星保底不会被五星重置,但与五星耦合时仍会在综合概率上产生细微的影响。此处的模型没有考虑四星和五星的耦合。 9 | 10 | 崩坏:星穹铁道的限定角色卡池与限定光锥卡池 **实际UP概率显著高于官方公示值** ,模型按照统计推理得到值建立。 11 | 12 | 崩坏:星穹铁道常驻跃迁中具有“平稳机制”,即角色和光锥两种类型的保底,GGanalysis 包没有提供这类模型,有需要可以借用 `GGanalysislib 包 `_ 为原神定义的模型进行计算。角色活动跃迁及光锥活动跃迁中的四星道具也有此类机制,由于其在“UP机制”后生效,对于四星UP道具抽取可忽略。 13 | 14 | 参数意义 15 | ------------------------ 16 | 17 | - ``item_num`` 需求物品个数,由于 sphinx autodoc 的 `bug `_ 在下面没有显示 18 | 19 | - ``multi_dist`` 是否以列表返回获取 1-item_num 个物品的所有分布列 20 | 21 | - ``item_pity`` 道具保底状态,通俗的叫法为水位、垫抽 22 | 23 | - ``up_pity`` UP道具保底状态,设为 1 即为玩家所说的大保底 24 | 25 | 基本模型 26 | ------------------------ 27 | 28 | **角色活动跃迁及常驻跃迁获得五星道具的模型** 29 | 30 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.common_5star 31 | 32 | **角色活动跃迁及常驻跃迁获得四星道具的模型** 33 | 34 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.common_4star 35 | 36 | 角色活动跃迁模型 37 | ------------------------ 38 | 39 | **角色活动跃迁获得UP五星角色的模型** 40 | 41 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_5star_character 42 | 43 | **角色活动跃迁获得任意UP四星角色的模型** 44 | 45 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_4star_character 46 | 47 | **角色活动跃迁获得特定UP四星角色的模型** 48 | 49 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_4star_specific_character 50 | 51 | .. code:: python 52 | 53 | import GGanalysis.games.honkai_star_rail as SR 54 | # 崩坏:星穹铁道角色池的计算 55 | print('角色池在垫了20抽,有大保底的情况下抽3个UP五星抽数的分布') 56 | dist_c = SR.up_5star_character(item_num=3, item_pity=20, up_pity=1) 57 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 58 | 59 | 光锥活动跃迁模型 60 | ------------------------ 61 | 62 | **光锥活动跃迁获得五星光锥的模型** 63 | 64 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.common_5star_weapon 65 | 66 | **光锥活动跃迁获得UP五星光锥的模型** 67 | 68 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_5star_weapon 69 | 70 | **光锥活动跃迁获得四星光锥的模型** 71 | 72 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.common_4star_weapon 73 | 74 | **光锥活动跃迁获得UP四星光锥的模型** 75 | 76 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_4star_weapon 77 | 78 | **光锥活动跃迁获得特定UP四星光锥的模型** 79 | 80 | .. automethod:: GGanalysis.games.honkai_star_rail.gacha_model.up_4star_specific_weapon 81 | -------------------------------------------------------------------------------- /docs/source/games/honkai_star_rail/index.rst: -------------------------------------------------------------------------------- 1 | 崩坏:星穹铁道 2 | ======================== 3 | .. toctree:: 4 | :maxdepth: 1 5 | :caption: 目录 6 | 7 | gacha_models 8 | relic_models 9 | -------------------------------------------------------------------------------- /docs/source/games/honkai_star_rail/relic_models.rst: -------------------------------------------------------------------------------- 1 | 崩坏:星穹铁道遗器模型 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ -------------------------------------------------------------------------------- /docs/source/games/index.rst: -------------------------------------------------------------------------------- 1 | 支持的游戏 2 | ======================== 3 | .. toctree:: 4 | :maxdepth: 2 5 | :caption: 目录 6 | 7 | genshin_impact/index 8 | honkai_star_rail/index 9 | arknights/index 10 | zenless_zone_zero/index 11 | azur_lane/index 12 | wuthering_waves/index 13 | blue_archive/index 14 | reverse_1999/index 15 | alchemy_stars/index 16 | girls_frontline2_exilium/index -------------------------------------------------------------------------------- /docs/source/games/reverse_1999/index.rst: -------------------------------------------------------------------------------- 1 | 重返未来:1999 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ 5 | -------------------------------------------------------------------------------- /docs/source/games/wuthering_waves/index.rst: -------------------------------------------------------------------------------- 1 | 鸣潮 2 | ======================== 3 | 4 | 文档施工中... 模型定义文件 `在这个位置 `_ 5 | -------------------------------------------------------------------------------- /docs/source/games/zenless_zone_zero/index.rst: -------------------------------------------------------------------------------- 1 | 绝区零抽卡模型 2 | ======================== 3 | 4 | GGanalysis 使用基本的抽卡模板模型结合 `基于700万抽数据统计的绝区零抽卡系统 `_ 定义了一系列可以直接取用的抽卡模型。 5 | 6 | .. attention:: 7 | 8 | 绝区零的 **A级保底会被S级重置**,与S级耦合时会对A级在综合概率上产生明显的影响。 9 | 单独的A级分布模型没有考虑A级和S级的耦合,计算得到A级概率是偏高的。计算S级和A级耦合后情况的代码位于 `此处 `_ 。 10 | 11 | 绝区零的常驻卡池中具有和原神与崩坏:星穹铁道都不一样的“平稳机制”,虽然也保证能在有限抽数内必定角色和武器两种类型的道具, 12 | 但绝区零根据抽到的道具数而不是已经投入的抽数进行判断。 13 | 若当前已有连续2个同类别S级物品,下个S级物品是另一类别的概率会大幅提高,观测到至多连续出3个同类别S级物品。 14 | 对于A级物品则为当已有连续4个同类别A级物品,下个A级物品是另一类别的概率会大幅提高,观测到至多连续出5个同类别A级物品。 15 | 16 | 参数意义 17 | ------------------------ 18 | 19 | - ``item_num`` 需求物品个数,由于 sphinx autodoc 的 `bug `_ 在下面没有显示 20 | 21 | - ``multi_dist`` 是否以列表返回获取 1-item_num 个物品的所有分布列 22 | 23 | - ``item_pity`` 道具保底状态,通俗的叫法为水位、垫抽 24 | 25 | - ``up_pity`` UP道具保底状态,设为 1 即为玩家所说的大保底 26 | 27 | 基本模型 28 | ------------------------ 29 | 30 | **角色池及常驻池获得S级道具的模型** 31 | 32 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.common_5star 33 | 34 | **角色池及常驻池获得A级道具的模型** 35 | 36 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.common_4star 37 | 38 | 角色池模型 39 | ------------------------ 40 | 41 | **角色池获得UPS级角色的模型** 42 | 43 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_5star_character 44 | 45 | **角色池获得任意UPA级角色的模型** 46 | 47 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_4star_character 48 | 49 | **角色池获得特定UPA级角色的模型** 50 | 51 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_4star_specific_character 52 | 53 | .. code:: python 54 | 55 | import GGanalysis.games.zenless_zone_zero as SR 56 | # 绝区零角色池的计算 57 | print('角色池在垫了20抽,有大保底的情况下抽3个UPS级抽数的分布') 58 | dist_c = SR.up_5star_character(item_num=3, item_pity=20, up_pity=1) 59 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 60 | 61 | 武器池模型 62 | ------------------------ 63 | 64 | **武器池获得S级武器的模型** 65 | 66 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.common_5star_weapon 67 | 68 | **武器池获得UPS级武器的模型** 69 | 70 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_5star_weapon 71 | 72 | **武器池获得A级武器的模型** 73 | 74 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.common_4star_weapon 75 | 76 | **武器池获得UPA级武器的模型** 77 | 78 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_4star_weapon 79 | 80 | **武器池获得特定UPA级武器的模型** 81 | 82 | .. automethod:: GGanalysis.games.zenless_zone_zero.gacha_model.up_4star_specific_weapon 83 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. GGanalysis documentation master file, created by 2 | sphinx-quickstart on Tue Mar 28 21:50:04 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 欢迎使用 GGanalysis 工具包! 7 | ====================================== 8 | 9 | GGanalysis 是一个概率计算工具包,主要用于解决各类游戏抽卡概率计算问题。 10 | 11 | 对于计算获取抽卡游戏中的某物品概率,工具包提供快速而便捷的抽卡游戏概率计算模块,既可以调用预设好的抽卡概率计算模型,也可以方便的为新游戏自定义模型。 12 | 同时,工具包也提供和抽卡概率计算模型配套的画图程序。 13 | 14 | GGanalysis 也提供计算极端情况发生的平均回归时、计算广义 CouponCollection 问题概率、计算多种道具耦合、类似原神圣遗物这样的道具的线性加权打分分布计算。 15 | 16 | .. admonition:: **工具包的名字是怎么起的?** 17 | :class: note 18 | 19 | GGanalysis 名称中的 GG 可以理解为 Gacha Game,也可以理解为 Good Game,也可以认为代表了构建这个包的初衷是为了分析 Genshin Gacha。 20 | 至于 analysis ,就只是单纯的分析啦! 21 | 22 | .. toctree:: 23 | :maxdepth: 2 24 | :caption: 上手文档 25 | 26 | start_using/index 27 | reference_manual/index 28 | 29 | .. toctree:: 30 | :maxdepth: 2 31 | :caption: 具体游戏支持 32 | 33 | games/index 34 | 35 | .. toctree:: 36 | :maxdepth: 2 37 | :caption: 抽卡导论 38 | 39 | introduction_to_gacha/index 40 | .. Indices and tables 41 | .. ================== 42 | 43 | .. * :ref:`genindex` 44 | .. * :ref:`modindex` 45 | .. * :ref:`search` 46 | -------------------------------------------------------------------------------- /docs/source/introduction_to_gacha/feedback_item_problem.rst: -------------------------------------------------------------------------------- 1 | 道具返还问题 2 | ======================== 3 | 4 | 这类问题被总结为消耗投入物品,以一定概率返还一系列物品的一系列问题,可以总结出两种问题: 5 | 6 | - 部分返还物品能转换为投入物品的情况下,计算相比不返还情况的投入物品等效打折系数 7 | 8 | - 部分返还物品能转换为投入物品的情况下,计算相比不返还情况的其他返还道具的额外获取系数 9 | 10 | 这两种问题可以相互转化,并且计算都较为简单。而对于有类似保底系统的情况来说,在讨论平稳分布的意义下,可以使用综合概率来进行计算返还期望。 11 | 12 | 处理嵌套返还时(即返还的物品还可以继续转化的情况),按照树结构展开,最后统计在叶子节点处能获得的道具,加和到一起即为最终可转化的值。\ 13 | 但如遇到返还道具能转换为投入物品的情况,不进行展开,最后将这部分返还的投入物品从初始投入中扣除即可得到平稳分布意义下投入和返还物品的关系。 -------------------------------------------------------------------------------- /docs/source/introduction_to_gacha/foundations.rst: -------------------------------------------------------------------------------- 1 | 基本概念约定 2 | ======================== 3 | 4 | 简单抽卡模型 5 | ------------------------ 6 | 7 | 定义每次抽卡至多获得一个道具,每次获得道具所需抽数为独立同分布的随机变量 :math:`X` ,且所有概率都分布在正整数范围上,那么此抽卡模型为简单抽卡模型。 8 | 如每次抽卡都是独立伯努利试验,固定概率为 :math:`P` 的抽卡模型是简单抽卡模型,其获得道具所需抽数分布为几何分布。 9 | 10 | .. note:: 11 | 12 | 现实情况中很少出现一抽能同时获得多个道具的情况,将讨论局限于简单抽卡模型是有好处的。 13 | 如简单抽卡模型中,可以将所有当前条件概率的表述等价转化为当前获取道具所需抽数分布的形式,同理也可以反向转换。这样的性质可以为很多操作带来方便。 14 | 以 :math:`E(X)` 表示模型获得道具的期望,:math:`P_{avg}(X)` 表示模型获得道具的综合概率。 15 | 16 | 简单保底抽卡模型 17 | ------------------------ 18 | 19 | 如果一个简单抽卡模型获得道具所需抽数是具有 **有限支持** (finite support)的,即随机变量的概率分布在有限个值上的取值不为零,有限个值之外的所有其他值上,概率分布为零。则将这种抽卡模型称为 **简单保底抽卡模型**。 20 | 常见带保底的抽卡模型大都可以归类为简单保底抽卡模型。例如原神获取五星物品的模型,每次获得五星物品所耗费抽数都是独立同分布的,并在有限抽内一定能获得五星物品。 21 | 22 | 简单保底抽卡模型的性质 23 | -------------------------- 24 | 25 | .. admonition:: 符号约定 26 | :class: note 27 | 28 | :math:`p_i` 表示在前 ``i-1`` 抽没有获得道具,第 ``i`` 抽获得道具的条件概率。 29 | 30 | :math:`P(X=i)` 表示获得一个道具时,恰好用了 ``i`` 抽的概率。或 :math:`X` 的分布律在 ``i`` 处的值。 31 | 32 | :math:`n` 表示硬保底抽数,也即简单保底抽卡模型至多使用多少抽才可以获得道具,也是随机变量分布有概率位置的最大值。 33 | 34 | **求简单保底抽卡模型的期望与综合概率** 35 | 36 | 简单保底抽卡模型的期望为 :math:`E(X)=\sum_{k=1}^{n}{p_k\cdot k}` 。其综合概率 :math:`P_{avg}(X)=1/E(X)` ,意义为同一个玩家进行了无穷次抽卡尝试后道具出现的频数。 37 | 38 | **已知条件概率求抽数分布** 39 | 40 | 获得一个道具时,恰好花费 ``i`` 抽的概率 :math:`P(X=i)=p_i\prod_{n=1}^{i-1}{(1-p_n)}` 41 | 42 | **已知抽数分布求条件概率** 43 | 44 | 在已经有 ``i-1`` 抽没有获得道具的情况下,第 ``i`` 抽获得道具的条件概率 :math:`p_i=\frac{P(X=i)}{\sum_{k=i}^{n}{P(X=k)}}` 45 | 46 | .. note:: 47 | 48 | 简单保底抽卡模型第 ``n`` 抽的位置对应条件概率为1,即保底位置一定能获得道具。 49 | 50 | .. **获得多个道具时所需抽数分布** 51 | 52 | .. 实际情况中也关心要获取多个道具时所需抽数的分布,即求获取 :math:`n` 个道具所用抽数这个随机变量的分布 :math:`X_{total}=X_1+X_2+...+X_n` 。 53 | .. 已知获取一个道具的分布情况下,利用动态规划或是卷积求获得多个道具的分布是非常容易的。 54 | 55 | .. .. note:: 56 | 57 | .. 继续以原神五星的抽卡模型举例,已知抽一个五星道具所需的抽数分布,现在想知道如果连续抽。 58 | 59 | .. 说明一下问题,通俗举例,然后可以用随机变量相加解释。 60 | .. 可以采用的方法有很多,模拟、转移矩阵、动态规划、卷积。 61 | .. 先以获得两个道具的抽数分布举例 62 | .. :math:`P(X_{UP}=i)=0.5\cdot P(X_1=i)+0.5\cdot P(X_2=i)` 63 | 64 | 65 | .. 两类问题(这部分拆到运气衡量部分) 66 | .. ------------------------ 67 | 68 | .. 一个是抽n个道具,需要花费抽数的分布 69 | 70 | .. 一个是投入k抽,能抽到道具个数的分布 -------------------------------------------------------------------------------- /docs/source/introduction_to_gacha/index.rst: -------------------------------------------------------------------------------- 1 | 抽卡导论 2 | ======================== 3 | 4 | 一直想要编写一些文字来说明如何为特定的抽卡问题进行建模,或者设计高效的算法,\ 5 | 并给出一步步的数学证明说明为什么这样做是对的。\ 6 | 这部分内容计划收集大量和抽卡相关的抽象问题,并给出在具体游戏中的对应,并给出部分代码和使用 GGanalysis 工具包解决具体问题的例子。\ 7 | 我想把这部分集合了大量抽卡相关的问题的内容叫做 **抽卡导论(Introduction to Gacha)**。 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: 目录 12 | 13 | foundations 14 | statistical_modeling_methods 15 | feedback_item_problem -------------------------------------------------------------------------------- /docs/source/introduction_to_gacha/statistical_modeling_methods.rst: -------------------------------------------------------------------------------- 1 | 简单保底抽卡模型的统计建模方法 2 | ================================ 3 | 4 | 我们讨论简单保底抽卡模型应该怎样进行统计,因为这类模型是在当前流行的抽卡游戏中应用最广的模式(原神、崩坏:星穹铁道、明日方舟等)。 5 | 如果出现一个具体抽卡机制不明确的新游戏,不妨先认为其采用简单保底抽卡模型,如果后续发现不能很好建模再进行其他尝试。 6 | 7 | 进行玩家数据统计的困难 8 | -------------------------- 9 | 10 | 对于简单保底抽卡模型,第一步肯定希望检验其综合概率是否准确。 11 | 综合概率的数学意义是当同一个玩家进行了无穷次抽卡尝试后道具出现的频数。 12 | 如果想要对道具的综合概率进行统计检验,显然抽无穷次是不可能的。 13 | 要达到有效的统计量,只能通过使用多个抽数有限的玩家的记录进行统计。 14 | 而这样面临的问题有三: 15 | 16 | 1. **无法收集到足够多的玩家数据** 如果你想统计的的是一个小众游戏,那么你将面临无数据可用的情况。 17 | 没有人建设抽卡数据统计网站,没有玩家联系你提供抽卡数据,甚至连在视频网站上寻找主播抽卡视频都是一种奢望。 18 | 对于小众游戏,基本不能通过统计方法得到准确的抽卡模型。 19 | 20 | 2. **采样到的玩家数据有系统性偏差** 会提供抽卡数据的玩家,大概率是还在玩游戏的玩家, 21 | 而如果玩家抽卡时运气太差则会倾向于不继续玩游戏,能被采样的玩家更多的是运气好的“幸存者”; 22 | 还存在“刷初始号”的玩家,他们会反复注册账号,利用游戏给新账号赠送的抽数抽取直到获取道具; 23 | 有时候获取到的抽卡记录会被截断,记录中第一个道具耗费的抽数记录不一定完整。 24 | 这三种情况会使得统计综合概率偏高。 25 | 同时,提供数据的玩家出于炫耀或是诉苦的心理,其获得道具的表现也更极端:运气好的玩家和运气差的玩家占比相比实际更高。 26 | 不过如果只采用投入总抽数高的玩家的数据,并剔除每位玩家记录最初的几个道具,可以大大缓解这些现象。 27 | 28 | 3. **使用的统计量需要保证无偏** 行千里,先定方向。如果在统计中不使用无偏统计量,获得的数据越多反而会让人向错误的方向走更远。 29 | 无偏统计量的选择会在 :ref:`下一节 ` 中解释。 30 | 31 | 而如果能够解决以上三个问题,通过统计破解简单保底抽卡模型是相当容易的。 32 | 33 | .. admonition:: 常见问题 34 | :class: note 35 | 36 | 对于原神这类有“大小保底”设定的游戏,有的朋友认为玩家会一直抽卡直到抽到UP角色,而这会引入系统偏差使得统计到的UP角色占比偏高。 37 | 而实际上这并不会,这个问题等价于:一直生孩子直到生出男孩会不会影响性别比例。一个经典的停时问题,实际上并不会对概率有影响。 38 | 39 | .. _unbiased_statistic_for_simple_pity_model: 40 | 41 | 简单保底抽卡模型综合概率的无偏统计量 42 | ---------------------------------------- 43 | 44 | 通过使用多个抽数有限的玩家的记录对综合概率进行统计时,如何选取统计量是有讲究的。 45 | 最朴素的方法是直接统计所有玩家记录中出现的道具总数,然后除以所有玩家的总抽数作为综合概率的估计量。 46 | 但这对于简单保底抽卡模型来说是有偏的:道具有概率上升段,存在玩家末尾位置抽数还没有到达概率开始上升阶段,但这一段的抽数依旧被纳入统计,估计出的综合概率一定是偏小的。 47 | 48 | 一个改进的方法是,不计玩家记录中获得最后一个道具后剩余的抽数。 49 | 统计所有记录中获得每个道具对应所用的抽数,将其加和作为统计抽数,然后将记录中的总道具数除以统计抽数用于估计综合概率。 50 | 51 | 拿原神的抽卡系统举例,原神中获取五星道具至多需要抽90抽。 52 | 如一个玩家总计抽卡200抽获得了5个五星,第1个五星耗费70抽,第2个五星耗费10抽,第3个五星耗费80抽,第4个五星耗费20抽,第5个五星耗费10抽,获得第5个五星后还剩10抽没有再获得五星。 53 | 此时获得总共5个五星,统计抽数190抽,估计综合概率为 :math:`\hat P_{avg}(X)=4/180 \approx 2.63\%`。 54 | 55 | 这样似乎得到了一个相当合理的统计量,但实际上这个方法也是有偏的,会使得统计得到的综合概率偏大。 56 | 57 | .. image:: images/biased_estimator_for_simple_pity_model.svg 58 | :alt: 简单保底抽卡模型在到达保底前截断会导致有偏 59 | :width: 800px 60 | :align: center 61 | 62 | 为了直观说明这个问题,继续拿原神的抽卡系统举例: 63 | 64 | 假设统计样本中的所有玩家都只抽30抽,那么所有观察到的五星所用抽数都必定小于30。 65 | 在这种情况下如果只记录获得每个道具对应所用的抽数,那么获得五星道具所需抽数超过30的可能性被忽略了。 66 | 此时统计得到的综合概率必定大于3.33%,和实际的1.605%差异很大。 67 | 68 | 消除这样的偏误的方法很简单:对于一个硬保底位置为 :math:`n` 的简单保底抽卡系统,道具只要满足其自身花费抽数加上道具获取位置到抽卡记录结束位置的抽数大于等于硬保底抽数,即可被认为可无偏采样。 69 | 统计所有记录中可无偏采样道具对应所用的抽数,将其加和作为统计抽数,然后将记录中的总可无偏采样道具数除以统计抽数用于估计综合概率。 70 | 71 | 继续刚才的例子,玩家总计抽卡200抽获得了5个五星,第1个五星耗费70抽,第2个五星耗费10抽,第3个五星耗费80抽,第4个五星耗费20抽,第5个五星耗费10抽,获得第5个五星后还剩10抽没有再获得五星。 72 | 其中第1/2/3个五星均为可无偏采样五星。而第4个五星耗费抽数和剩余抽数之和为40,第5个五星耗费抽数和剩余抽数之和为20,均不满足大于等于保底数90抽的条件,被剔除。 73 | 此时统计总共3个可无偏采样五星,统计抽数160抽,估计综合概率为 :math:`\hat P_{avg}(X)=3/160=1.875\%`。 74 | 75 | 这样得到的简单保底抽卡系统的综合概率统计量是无偏的。且相比统计时的采样偏误,很多时候将有偏统计量切换为无偏统计量对误差的减小更为明细。 76 | 77 | 当然,如果 **不需要画出玩家获得道具花费抽数的分布**,直接统计玩家获得道具的条件概率是更为方便且没有系统偏差的办法。这种办法可以较为细致的研究不同星级道具间的互相影响。并且不同于依靠获得分布的方法为了保证无偏需要丢弃一部分数据,这样的统计方法对数据的利用更充分,且可以自由的统计不同时段的区别。 78 | 79 | 对于原神来说,以当前已有N抽没有获得五星道具、已有M抽没有获得四星道具为状态,统计每种记录状态下进行抽卡时获得道具星级的情况。最后通过该状态下抽卡获得某星级的次数除以该状态下总共统计的抽卡次数来估计该状态下抽卡获得某星级的概率。 80 | 81 | 理解玩家对游戏概率的感受 82 | ---------------------------------------- 83 | 84 | 在抽卡游戏中,玩家对游戏抽卡系统概率欺诈的质疑是常见的。 85 | 毕竟玩家基数足够大,出现极端情况几乎是不可避免的: 86 | 一定有部分玩家运气极好,一定有部分玩家运气极差。 87 | 运气不佳的玩家可能会感觉自己受到了不公正的对待,并倾向于在社交媒体上表达自己的不满,同时也能勾起广大网友的不好回忆引发共鸣。 88 | 89 | 作为游戏抽卡系统的统计者,应当了解玩家社区的声音不能代替大样本统计数据,但同时也应该重视玩家社区中使用自己收集的小样本统计提出的合理质疑: 90 | 游戏抽卡系统概率造假存在先例。而即使实际的抽卡系统中不存在概率欺诈,收集更多极端情况对解析抽卡系统也是有益的。 91 | 对于那些已经通过大量数据验证并建立了精确模型的游戏,统计者应该理解自己和普通玩家是不对等的,大量玩家并没有对抽卡系统有足够了解。 92 | 面对玩家抱怨游戏针对了他们时,要理解普通玩家只是希望享受游戏的乐趣,抽卡却让他们相当糟心。 93 | 统计者此时不必解释理论上如何怎样,而应提供更多的支持和安慰:游戏不是生活的全部,放宽心态。 94 | 抽卡模式一定会给一部分人带来糟糕的体验,但市场告诉我们抽卡模式就是赚钱。 95 | 未来仍会有大量游戏采用抽卡模式,在这样的预期下,统计者应当向大众传播正确的概率认知和方法论, 96 | 帮助大众理解被抽卡模式模糊化的实际定价,努力破除信息不对称。 -------------------------------------------------------------------------------- /docs/source/reference_manual/basic_tools.rst: -------------------------------------------------------------------------------- 1 | 基础工具 2 | ====================================== 3 | 4 | 随机变量分布处理工具 5 | ---------------------- 6 | 7 | .. autoclass:: GGanalysis.distribution_1d.FiniteDist 8 | :members: 9 | 10 | .. automethod:: GGanalysis.distribution_1d.pad_zero 11 | 12 | .. automethod:: GGanalysis.distribution_1d.cut_dist 13 | 14 | .. automethod:: GGanalysis.distribution_1d.calc_expectation 15 | 16 | .. automethod:: GGanalysis.distribution_1d.calc_variance 17 | 18 | .. automethod:: GGanalysis.distribution_1d.dist2cdf 19 | 20 | .. automethod:: GGanalysis.distribution_1d.cdf2dist 21 | 22 | .. automethod:: GGanalysis.distribution_1d.linear_p_increase 23 | 24 | .. automethod:: GGanalysis.distribution_1d.p2dist 25 | 26 | .. automethod:: GGanalysis.distribution_1d.dist2p 27 | 28 | .. automethod:: GGanalysis.distribution_1d.p2exp 29 | 30 | .. automethod:: GGanalysis.distribution_1d.p2var 31 | 32 | 33 | 马尔可夫链处理工具 34 | ------------------------- 35 | 36 | .. automethod:: GGanalysis.markov_method.table2matrix 37 | 38 | .. automethod:: GGanalysis.markov_method.calc_stationary_distribution 39 | 40 | .. automethod:: GGanalysis.markov_method.multi_item_rarity 41 | 42 | .. autoclass:: GGanalysis.markov_method.PriorityPitySystem 43 | :members: 44 | 45 | 集齐问题处理工具 46 | ------------------------- 47 | 48 | .. autoclass:: GGanalysis.coupon_collection.GeneralCouponCollection 49 | :members: 50 | 51 | .. automethod:: GGanalysis.coupon_collection.get_equal_coupon_collection_exp 52 | -------------------------------------------------------------------------------- /docs/source/reference_manual/index.rst: -------------------------------------------------------------------------------- 1 | 参考手册 2 | ====================================== 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | basic_tools -------------------------------------------------------------------------------- /docs/source/start_using/basic_concepts.rst: -------------------------------------------------------------------------------- 1 | 基本概念 2 | ======================== 3 | 4 | FiniteDist 类型 5 | ------------------------ 6 | 7 | .. .. autoclass:: GGanalysis.FiniteDist 8 | .. :special-members: __init__, __setitem__, __getitem__, __add__, __mul__, __rmul__, __truediv__, __pow__, __str__, __len__ 9 | .. :members: 10 | 11 | :class:`~GGanalysis.FiniteDist` 是非常重要的类型,GGanalysis 工具包的计算功能建立在这个类型之上,\ 12 | 大部分模型返回的值都以 ``FiniteDist`` 类型表示。 13 | 14 | .. admonition:: 如何理解 FiniteDist 类型 15 | :class: note 16 | 17 | 可以认为 GGanalysis 中的 FiniteDist 是对 numpy 数组的包装,核心是记录了离散随机变量的分布信息。 18 | 19 | **创建新的 FiniteDist 类型** 20 | 21 | FiniteDist 用于描述自然数位置上的有限长分布,用一个数组记录从0位置开始分布的概率。\ 22 | 可以视为记录了一个样本空间为自然数的随机变量的信息。 23 | 可以使用列表、numpy 数组或者另一个 ``FiniteDist`` 来创建新的 ``FiniteDist``。 24 | 25 | .. code:: Python 26 | 27 | import GGanalysis as gg 28 | import numpy as np 29 | 30 | dist_a = gg.FiniteDist([0, 0.5, 0.5]) 31 | dist_b = gg.FiniteDist(np.array([0, 1])) 32 | dist_c = gg.FiniteDist(dist_a) 33 | 34 | **从 FiniteDist 中提取信息** 35 | 36 | ``FiniteDist`` 类型的分布以 numpy 数组形式记录在 ``FiniteDist.dist`` 中。\ 37 | 同时可以从 ``FiniteDist`` 类型中直接获取分布的基本属性,如期望和方差。 38 | 39 | .. code:: Python 40 | 41 | print('dist_a 的分布数组', dist_a.dist) # [0. 0.5 0.5] 42 | print('dist_a 的期望', dist_a.exp) # 1.5 43 | print('dist_a 的方差', dist_a.var) # 0.25 44 | 45 | **用 FiniteDist 表达随机变量之和** 46 | 47 | ``FiniteDist`` 类型之间的 ``*`` 运算被定义为卷积,数学意义为这两个随机变量和的随机变量。 48 | 49 | 在抽卡游戏中,可以认为是获取了 A 道具后再获取 B 道具,两个事件叠加后所需抽数的分布。 50 | 51 | .. code:: Python 52 | 53 | dist_c = dist_a * dist_b 54 | print('混合后的分布', dist_c.dist) # [0. 0. 0.5 0.5] 55 | 56 | **用 FiniteDist 表达独立同分布随机变量之和** 57 | 58 | ``FiniteDist`` 类型也定义了 ``**`` 运算,返回分布为指定个数独立同分布随机变量分布相卷积的 ``FiniteDist`` 对象。 59 | 60 | .. code:: Python 61 | 62 | dist_c = dist_b ** 3 63 | print('乘方后的分布', dist_c.dist) # [0. 0. 0. 1.] 64 | 65 | **对分布进行数量乘** 66 | 67 | ``FiniteDist`` 类型与数字类型之间的 ``*`` 运算是对 ``FiniteDist.dist`` 的简单数乘。\ 68 | 请注意数量乘是为了方便进行一些操作时提供的运算,为满足归一性需要后续继续处理。 69 | 70 | .. code:: Python 71 | 72 | dist_c = dist_a * 2 73 | print('数量乘后的分布数组', dist_c.dist) # [0. 1. 1.] 74 | 75 | **对分布进行数量加** 76 | 77 | ``FiniteDist`` 类型之间的 ``+`` 运算被定义为其分布数组按 0 位置对齐直接相加。\ 78 | 请注意数量加是为了方便进行一些操作时提供的运算,为满足归一性需要后续继续处理。 79 | 80 | .. code:: Python 81 | 82 | dist_c = dist_a + dist_b 83 | print('数量加的分布数组', dist_c.dist) # [0, 1.5, 0.5] 84 | 85 | CommonGachaModel 类型 86 | ------------------------ 87 | 88 | :class:`~GGanalysis.CommonGachaModel` 用于描述每次获得指定道具所需抽数分布都是独立的抽卡模型。 89 | 是大部分预定义抽卡模型的基类。 90 | 通过 ``CommonGachaModel`` 定义的类通常接收所需道具的个数,当前条件信息,最后以 FiniteDist 类型返回所需抽数分布。 91 | 92 | ``CommonGachaModel`` 定义时接受抽卡层的组合,以此构建按顺序复合各个抽卡层,自动推理出组合后的概率分布\ 93 | ,以此快捷地构造出复杂的抽卡模型。当前支持的抽卡层有: 94 | 95 | 1. ``Pity_layer`` 保底抽卡层,实现每抽获取物品的概率仅和当前至多多少抽没有获取过物品相关的抽卡模型。 96 | 97 | 2. ``Bernoulli_layer`` 伯努利抽卡层,实现每次抽卡获取物品的概率都是相互独立并有同样概率的抽卡模型。 98 | 99 | 3. ``Markov_layer`` 马尔科夫抽卡层,实现每次抽卡都按一定概率在状态图上进行转移的抽卡模型。保底抽卡层是马尔科夫抽卡层的特例。 100 | 101 | 4. ``Coupon_Collector_layer`` 均等概率集齐道具层,实现每次抽卡随机获得某种类的道具,若干不同种类道具均分概率,当集齐一定种类的代币后获得物品的抽卡模型。(注意:目前集齐道具层的功能已经可以使用,但还未经过充分的测试) 102 | 103 | 5. ``DynamicProgrammingLayer`` 动态规划层,通过传入动态规划函数实现更灵活的功能实现和抽卡层组合,自由度比马尔科夫抽卡层更高。(开发中,还未经过测试) 104 | 105 | .. .. autoclass:: GGanalysis.FiniteDist 106 | .. :members: 107 | 108 | .. .. autoclass:: GGanalysis.CommonGachaModel 109 | .. :members: -------------------------------------------------------------------------------- /docs/source/start_using/check_gacha_plan.rst: -------------------------------------------------------------------------------- 1 | 检查抽卡计划可行性 2 | ======================== 3 | 4 | 在现实中,玩家会因为游戏中道具会分时间段开放抽取,不同时间段抽卡资源投放也不同,经常需要评估一个抽卡规划的可行性。 5 | 我们可以用在限制条件下能有多大概率达成目标来描述计划的可行性。 6 | 7 | .. admonition:: 抽卡问题例子 8 | :class: note 9 | 10 | 以绝区零为例,假设1.0版本可以获得200抽,这时候计划在1.0版本下半抽一个限定S级角色加一个限定S级武器, 11 | 1.1版本上半可获得74抽,计划在抽一个限定S级角色,1.1版本下半可获得40抽,计划此时再抽一个限定S级角色加一个限定S级武器, 12 | 那么按照计划把这些武器和角色都抽出来的概率有多大? 13 | 14 | 在这个问题中要计算多大概率可以达到目标,本质上是计算能在各个阶段的抽数限制下都能达成目标的玩家占比, 15 | 只需要 **在每个阶段中剔除超出抽数限制部分玩家对应概率空间** 即可。 16 | 以下代码给出了计算刚才例子中抽卡计划达成概率的可能性及达成计划的玩家中抽数花费的分布。 17 | 18 | .. code:: Python 19 | 20 | from GGanalysis import FiniteDist 21 | import GGanalysis.games.zenless_zone_zero as ZZZ 22 | # 设定要抽的角色数量/武器数量以及阶段预算 23 | tasks = [ 24 | [1, 1, 200], 25 | [1, 0, 74], 26 | [1, 1, 40], 27 | ] 28 | total_c = 0 29 | total_w = 0 30 | total_pulls = 0 31 | ans_dist = FiniteDist([1]) 32 | for (num_c, num_w, task_pulls) in tasks: 33 | total_c += num_c 34 | total_w += num_w 35 | total_pulls += task_pulls 36 | ans_dist *= ZZZ.up_5star_character(num_c) * ZZZ.up_5star_weapon(num_w) 37 | ans_dist = FiniteDist(ans_dist[:total_pulls+1]) 38 | print("成功概率", sum(ans_dist.dist)) 39 | ans_dist = ans_dist.normalized() # 归一化 40 | print("成功玩家期望抽数消耗", ans_dist.exp) 41 | full_dist = ZZZ.up_5star_character(total_c)*ZZZ.up_5star_weapon(total_w) 42 | print("获得计划道具期望", full_dist.exp) 43 | 44 | .. attention:: 45 | 给出的计算方法只适合于 **每次获得道具独立同分布** 的情况,对于大部分的抽卡游戏来说这个计算方法是准确的。 46 | 此方法对于原神5.0版本后角色池抽卡不再适用,因为引入的「捕获明光」机制使得每次获得限定五星不再是独立的,采用此方法会低估概率。 -------------------------------------------------------------------------------- /docs/source/start_using/custom_gacha_model.rst: -------------------------------------------------------------------------------- 1 | 自定义抽卡模型 2 | ======================== 3 | 4 | .. admonition:: 抽卡问题例子 5 | :class: note 6 | 7 | 假设有一个游戏有这样的抽卡系统:每抽有 10% 概率获得道具, 90% 概率获得垃圾。当连续 3 抽都没有获得道具时,获得道具的概率上升到 60%,连续 4 抽都没有获得道具时,获得道具的概率上升到 100%。 8 | 9 | 道具中一共有 5 种不同类别,每种类别均分概率。 10 | 11 | 这个例子中当一定抽数没有获得道具时,下次获得道具的概率会逐渐上升,直到概率上升为 100%。将这种模型称为软保底模型。 12 | 13 | 同时注意到,这个例子中获取了道具后,道具可能是 5 种道具中的任意一种,将这种等概率选择的模型称为伯努利模型。 14 | 15 | 通过预定义模板自定义抽卡模型 16 | ----------------------------- 17 | 18 | GGanalysis 中已经预定义好了结合保底抽卡模型和伯努利抽卡模型的模板模型 :class:`~GGanalysis.PityBernoulliModel` ,将当前参数传入其中即可获得对应模型。 19 | 20 | .. code:: Python 21 | 22 | import GGanalysis as gg 23 | # 定义一个星级带软保底,每个星级内有5种物品,想要获得其中特定一种的模型 24 | # 定义软保底概率上升表,第1-3抽概率0.1,第4抽概率0.6,第5抽保底 25 | pity_p = [0, 0.1, 0.1, 0.1, 0.6, 1] 26 | 27 | # 采用预定义的保底伯努利抽卡类 28 | gacha_model = gg.PityBernoulliModel(pity_p, 1/5) 29 | # 根据定义的类计算从零开始获取一个道具的分布,由于可能永远获得不了道具,分布是截断的 30 | dist = gacha_model(item_num=1, item_pity=0) 31 | 32 | 组合抽卡层自定义抽卡模型 33 | ------------------------ 34 | 35 | GGanalysis 也支持更细粒度的定制抽卡模型。可以继承 :class:`~GGanalysis.CommonGachaModel` 并组合不同抽卡层来设计自己的抽卡模型。 36 | 37 | 同样是解决上述抽卡问题,可以参照以下代码在自定义的抽卡模型中将保底抽卡层和伯努利抽卡层按顺序结合起来,就可以建立获取指定类别的道具的模型了。 38 | 39 | .. code:: Python 40 | 41 | import GGanalysis as gg 42 | # 定义一个星级带软保底,每个星级内有5种物品,想要获得其中特定一种的模型 43 | # 定义软保底概率上升表,第1-3抽概率0.1,第4抽概率0.6,第5抽保底 44 | pity_p = [0, 0.1, 0.1, 0.1, 0.6, 1] 45 | 46 | # 保底伯努利抽卡类 47 | class MyModel(gg.CommonGachaModel): 48 | # 限制期望的误差比例为 1e-8,达不到精度时分布截断位置为 1e5 49 | def __init__(self, pity_p, p, e_error = 1e-8, max_dist_len=1e5) -> None: 50 | super().__init__() 51 | # 增加保底抽卡层 52 | self.layers.append(gg.PityLayer(pity_p)) 53 | # 增加伯努利抽卡层 54 | self.layers.append(gg.BernoulliLayer(p, e_error, max_dist_len)) 55 | # 根据自定义的类计算从零开始获取一个道具的分布,由于可能永远获得不了道具,分布是截断的 56 | gacha_model = MyModel(pity_p, 1/5) 57 | # 关于 item_pity 等条件输入,如有需要可以参考预置类中 __call__ 和 _build_parameter_list 的写法 58 | dist = gacha_model(item_num=1) -------------------------------------------------------------------------------- /docs/source/start_using/environment_installation.rst: -------------------------------------------------------------------------------- 1 | 配置环境并安装工具包 2 | ======================== 3 | 4 | 安装 Python 5 | ------------------------ 6 | 7 | 工具包需要 ``python>=3.9``,可以从 `官方网站 `_ 下载并安装。 8 | 9 | 也可以将使用环境置于 Anaconda 环境中, `Anaconda下载 `_ `Miniconda下载 `_。 10 | 11 | .. attention:: 12 | 13 | 只使用抽卡概率计算相关工具时可以采用更老的 Python 版本(不推荐)。 14 | Python 3.9 之前的版本不支持工具包内类型提示写法,在导入计算相关包时会报错,可以选择手动修改报错部分类型提示代码解决问题。 15 | 16 | pip 安装 GGanalysis 17 | ------------------------ 18 | 19 | 如果不需要使用最新版,可以选择直接从 `PyPI `_ 安装。 20 | 21 | .. code:: shell 22 | 23 | pip install GGanalysis 24 | 25 | 手动安装 GGanalysis 26 | ------------------------ 27 | 28 | 如果想要使用 `GitHub `_ 上当前最新版本的代码,可以从仓库拉取并手动安装。 29 | 在本地机器安装了 ``git`` 的情况下,可以直接使用 ``git clone`` 命令,并使用 ``pip`` 安装,一切正常的话依赖包会自动安装。 30 | 31 | GGanalysis 依赖的 Python 包如下: 32 | 33 | - `numpy `_ 34 | - `scipy `_ 35 | - `matplotlib `_ 36 | 37 | .. code:: shell 38 | 39 | git clone https://github.com/OneBST/GGanalysis.git 40 | cd GGanalysis 41 | # 如果不需要编辑工具包内代码,直接执行以下命令,安装完成后 git 下载文件可删除 42 | pip install . 43 | # 如果需要编辑工具包内代码,加入 -e 选项,运行时直接引用当前位置的包,对代码的修改会实时反应 44 | pip install -e . 45 | 46 | 如果没有安装 ``git`` ,可以点击 `这个链接 `_ 下载压缩包, 47 | 解压后按照以上流程操作。(注意需要将 ``cd GGanalysis`` 改为 ``cd GGanalysis-main``) 48 | 49 | 安装画图使用字体 50 | ------------------------ 51 | 52 | .. attention:: 53 | 54 | 安装画图程序所需字体是不是必须的。如果只需要使用 GGanalysis 工具包进行概率计算,则可以略过此步骤。 55 | 56 | GGanalysis 工具包使用 `思源黑体 `_ , 57 | 请下载工具包使用的 `特定版本字体 `_ 并安装。 -------------------------------------------------------------------------------- /docs/source/start_using/index.rst: -------------------------------------------------------------------------------- 1 | 快速使用 2 | ======================== 3 | .. toctree:: 4 | :maxdepth: 1 5 | :caption: 目录 6 | 7 | environment_installation 8 | basic_concepts 9 | use_predefined_model 10 | custom_gacha_model 11 | check_gacha_plan 12 | stationary_distribution 13 | quick_visualization 14 | -------------------------------------------------------------------------------- /docs/source/start_using/quick_visualization.rst: -------------------------------------------------------------------------------- 1 | 快速可视化 2 | ======================== 3 | 4 | 绘制简略的概率质量函数图及累积质量函数图 5 | --------------------------------------------- 6 | 7 | .. code:: Python 8 | 9 | import GGanalysis.games.genshin_impact as GI 10 | # 获得原神抽一个UP五星角色+定轨抽特定UP五星武器的分布 11 | dist = GI.up_5star_character(item_num=1) * GI.up_5star_ep_weapon(item_num=1) 12 | # 导入绘图模块 13 | from GGanalysis.gacha_plot import DrawDistribution 14 | fig = DrawDistribution(dist, show_description=True) 15 | fig.show_two_graph(dpi=72) -------------------------------------------------------------------------------- /docs/source/start_using/stationary_distribution.rst: -------------------------------------------------------------------------------- 1 | 平稳分布时概率的计算工具 2 | ======================== 3 | 4 | 使用转移矩阵方法计算复合类型保底的概率 5 | ---------------------------------------- 6 | 7 | .. code:: Python 8 | 9 | # 以明日方舟为例,计算明日方舟六星、五星、四星、三星物品耦合后,各类物品的综合概率 10 | import GGanalysis as gg 11 | import GGanalysis.games.arknights as AK 12 | # 按优先级将道具概率表组成列表 13 | item_p_list = [AK.PITY_6STAR, AK.PITY_5STAR, [0, 0.5], [0, 0.4]] 14 | AK_probe = gg.PriorityPitySystem(item_p_list, extra_state=1, remove_pity=True) 15 | # 打印结果,转移矩阵的计算可能比较慢 16 | print(AK_probe.get_stationary_p()) # [0.02890628 0.08948246 0.49993432 0.38167693] 17 | 18 | 使用迭代方法计算平稳分布后n连抽获得k个道具概率 19 | -------------------------------------------------- 20 | 21 | .. code:: Python 22 | 23 | # 以原神10连获得多个五星道具为例 24 | import GGanalysis as gg 25 | import GGanalysis.games.genshin_impact as GI 26 | # 获得平稳后进行10连抽获得k个五星道具的分布 27 | ans = gg.multi_item_rarity(GI.PITY_5STAR, 10) 28 | -------------------------------------------------------------------------------- /docs/source/start_using/use_predefined_model.rst: -------------------------------------------------------------------------------- 1 | 使用预定义的抽卡模型 2 | ======================== 3 | 4 | 预定义的抽卡模型多以 :class:`~GGanalysis.CommonGachaModel` 为基类。 5 | 根据输入信息返回 :class:`~GGanalysis.FiniteDist` 类型的结果。 6 | 7 | 以原神为例说明如何使用预先定义好的抽卡模型。更多预定义模型可在 :ref:`原神抽卡模型 ` 中查阅。 8 | 9 | .. code:: Python 10 | 11 | # 导入预定义好的原神模块 12 | import GGanalysis.games.genshin_impact as GI 13 | # 原神角色池的计算 14 | dist_c = GI.up_5star_character(item_num=3, item_pity=20, up_pity=1) 15 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 16 | 17 | 以明日方舟为例说明如何使用预先定义好的抽卡模型。更多预定义模型可在 :ref:`明日方舟抽卡模型 ` 中查阅。 18 | 19 | .. code:: Python 20 | 21 | # 计算抽卡所需抽数分布律 以明日方舟为例 22 | import GGanalysis.games.arknights as AK 23 | # 普池双UP的计算 item_num是要抽多少个 item_pity是当前垫了多少抽,从零开始填0就行 24 | dist_c = AK.dual_up_specific_6star(item_num=3, item_pity=20) 25 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 26 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 抽卡游戏概率分析工具包-GGanalysis 2 | 3 | 本工具包为快速构建抽卡游戏抽卡模型设计,通过引入“[抽卡层](https://github.com/OneBST/GGanalysis/blob/main/GGanalysis/gacha_layers.py)”,以抽卡层的组合快速实现复杂的抽卡逻辑,并以较低的时间复杂度计算抽卡模型对应的分布。除了计算分布,工具包还提供一些将计算结果可视化的[绘图工具](https://github.com/OneBST/GGanalysis/blob/main/GGanalysis/plot_tools.py),并计划加入更多的针对各类抽卡问题的计算工具和设计工具。 4 | 5 | 对于原神圣遗物、崩铁遗器这类随机提升副词条的模型提供了[词条模拟工具](https://github.com/OneBST/GGanalysis/blob/main/GGanalysis/SimulationTools/scored_item_sim.py)和更快速且误差很小的[近似计算工具](https://github.com/OneBST/GGanalysis/blob/main/GGanalysis/ScoredItem/scored_item.py)。但请注意,这部分代码接口可能大幅改动。 6 | 7 | 工具包[在线文档](https://onebst.github.io/GGanalysis/)在持续完善中,使用交流QQ群:797922035。 8 | 9 | ## 安装方法 10 | 11 | 本工具包的依赖库很简单,只需要在 `Python>=3.9` 环境中安装`numpy`和`scipy`即可。如果需要使用工具包提供的画图代码,还需安装 `matplotlib`。如果你不需要使用最新版,还可以直接从[PyPI](https://pypi.org/project/GGanalysis/)安装。 12 | 13 | ``` shell 14 | pip install GGanalysis 15 | ``` 16 | 17 | 工具包目前还在开发中,如想安装本工具包最新版本,可以打开终端输入以下指令,安装完成后下载文件可以删除。 18 | 19 | ```shell 20 | git clone https://github.com/OneBST/GGanalysis.git 21 | cd GGanalysis 22 | pip install . 23 | ``` 24 | 25 | 画图时需要安装[思源黑体](https://github.com/adobe-fonts/source-han-sans),安装[对应版本](https://github.com/adobe-fonts/source-han-sans/releases/download/2.004R/SourceHanSansSC.zip)后即可使用,若出现找不到字体的情况,Windows 下检查 `C:/Windows/Fonts/` 下是否有以 `SourceHanSansSC` 开头的otf字体(Linux 则检查 `~/.local/share/fonts/`) 26 | 27 | 如果安装后还是找不到字体,请将 `GGanalysis/plot_tools.py` 内 `mpl.rcParams['font.family'] = 'Source Han Sans SC'` 自行修改为你指定的字体。 28 | 29 | ## 使用方法 30 | 31 | 可以在[在线文档](https://onebst.github.io/GGanalysis/)中查看详细使用指南,这里简单举例 32 | 33 | **使用定义好的抽卡模型计算抽卡所需抽数分布** 34 | 35 | ``` python 36 | # 计算抽卡所需抽数分布律 以原神为例 37 | import GGanalysis.games.genshin_impact as GI 38 | # 原神角色池的计算 39 | print('角色池在垫了20抽,有大保底的情况下抽3个UP五星抽数的分布') 40 | dist_c = GI.up_5star_character(item_num=3, item_pity=20, up_pity=1) 41 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 42 | 43 | # 计算抽卡所需抽数分布律 以明日方舟为例 44 | import GGanalysis.games.arknights as AK 45 | # 普池双UP的计算 item_num是要抽多少个 item_pity是当前垫了多少抽,从零开始填0就行 46 | dist_c = AK.dual_up_specific_6star(item_num=3, item_pity=20) 47 | print('期望为', dist_c.exp, '方差为', dist_c.var, '分布为', dist_c.dist) 48 | ``` 49 | 50 | **绘制简略的概率质量函数图及累积质量函数图** 51 | 52 | ``` python 53 | # 绘图前需要安装 matplotlib 以及需要的字体包 54 | import GGanalysis.games.genshin_impact as GI 55 | # 获得原神抽一个UP五星角色+定轨抽特定UP五星武器的分布 56 | dist = GI.up_5star_character(item_num=1) * GI.up_5star_ep_weapon(item_num=1) 57 | # 导入绘图模块 58 | from GGanalysis.gacha_plot import DrawDistribution 59 | fig = DrawDistribution(dist, dpi=72, show_description=True) 60 | fig.draw_two_graph() 61 | ``` 62 | 63 | 每个游戏的抽卡模型定义在 `GGanalysis/games/gamename/gacha_model.py` 文件中 64 | 65 | 每个游戏的绘图程序在项目 `GGanalysis/games/gamename/figure_plot.py` 文件下可参考 66 | 67 | ## 注意事项 68 | 69 | 目前工具包支持的抽卡层仅适用于满足马尔科夫性质的抽卡模型,即给定现在状态及过去所有状态的情况下,未来抽卡的结果仅仅依赖于当前状态,与过去的状态是独立的。不过好消息是,游戏的抽卡系统基本都满足这样的性质。 70 | 71 | 当前工具包能实现的抽卡模型是有限的,仅能实现能被给出的四种抽卡层组合出来的模型。对于类似“300井”等,在一定抽数后直接为玩家提供道具的模型,在本工具包框架下仅需简单修改即可。而对于类似不放回抽样的奖品堆模式、集齐碎片兑换模式等,还待之后继续扩展功能。 72 | 73 | 同时迭代方法计算n连抽获得k个道具概率尚未经过严格数学证明,使用时需要注意。 74 | 75 | ## 参与项目 76 | 77 | 你可以一起参与本项目的建设,添加更多游戏的支持,提交 pull request 时先选择提交到 `develop` 分支,确定无问题后会被合并到 `main` 分支。 78 | 79 | 如果要在本地进行开发,不推荐使用以上安装方法,请使用: 80 | 81 | ``` shell 82 | pip install -e . 83 | ``` 84 | 85 | 这样调用包时会使用本地文件位置的包,就可以随时使用本地更改过的版本了! -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | NAME = 'GGanalysis' 4 | DESCRIPTION = 'A simple and efficient computing package for gacha game analysis.' 5 | URL = 'https://github.com/OneBST/GGanalysis' 6 | EMAIL = 'onebst@foxmail.com' 7 | AUTHOR = 'OneBST' 8 | REQUIRES_PYTHON = '>=3.9.0' 9 | VERSION = '0.4.3' 10 | REQUIRED = [ 11 | 'numpy', 'scipy', 'matplotlib' 12 | ] 13 | 14 | setuptools.setup( 15 | name=NAME, 16 | version=VERSION, 17 | description=DESCRIPTION, 18 | author=AUTHOR, 19 | author_email=EMAIL, 20 | python_requires=REQUIRES_PYTHON, 21 | url=URL, 22 | packages=setuptools.find_packages(include=["GGanalysis*"]), 23 | install_requires=REQUIRED, 24 | include_package_data=True, 25 | license='MIT', 26 | ) --------------------------------------------------------------------------------