From 242826121db3d525b2aba2dbfe8297a132d12728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B4=9B=E6=B0=B4=E5=B1=85=E5=AE=A4?= Date: Sat, 15 Oct 2022 16:14:32 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E6=96=B0?= =?UTF-8?q?=E6=8A=BD=E5=8D=A1=E6=A8=A1=E6=8B=9F=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/gacha/banner.py | 75 ++++++++++++++++ modules/gacha/error.py | 14 +++ modules/gacha/player/banner.py | 78 +++++++++++++++++ modules/gacha/player/info.py | 30 +++++++ modules/gacha/pool.py | 27 ++++++ modules/gacha/system.py | 156 +++++++++++++++++++++++++++++++++ modules/gacha/utils.py | 26 ++++++ 7 files changed, 406 insertions(+) create mode 100644 modules/gacha/banner.py create mode 100644 modules/gacha/error.py create mode 100644 modules/gacha/player/banner.py create mode 100644 modules/gacha/player/info.py create mode 100644 modules/gacha/pool.py create mode 100644 modules/gacha/system.py create mode 100644 modules/gacha/utils.py diff --git a/modules/gacha/banner.py b/modules/gacha/banner.py new file mode 100644 index 00000000..d7319c7a --- /dev/null +++ b/modules/gacha/banner.py @@ -0,0 +1,75 @@ +from enum import Enum +from typing import List, Tuple + +from modules.gacha.error import GachaIllegalArgument +from modules.gacha.utils import lerp + + +class BannerType(Enum): + STANDARD = 0 + EVENT = 1 + WEAPON = 2 + + +class GachaBanner: + weight4 = ((1, 510), (8, 510), (10, 10000)) + weight5 = ((1, 60), (73, 60), (90, 10000)) + fallback_items3: List[int] = [ + 11301, + 11302, + 11306, + 12301, + 12302, + 12305, + 13303, + 14301, + 14302, + 14304, + 15301, + 15302, + 15304, + ] + # 硬编码三星武器 + banner_type: BannerType = BannerType.STANDARD + wish_max_progress: int = 0 + pool_balance_weights4: Tuple[int] = ((1, 255), (17, 255), (21, 10455)) + pool_balance_weights5: Tuple[int] = ((1, 30), (147, 150), (181, 10230)) + event_chance5: int = 50 + event_chance4: int = 50 + event_chance: int = -1 + rate_up_items5: List[int] = [] # UP五星 + fallback_items5_pool1: List[int] = [] # 基础五星角色 + fallback_items5_pool2: List[int] = [] # 基础五星武器 + rate_up_items4: List[int] = [] # UP四星 + fallback_items4_pool1: List[int] = [] # 基础四星角色 + fallback_items4_pool2: List[int] = [] # 基础四星武器 + auto_strip_rate_up_from_fallback: bool = True + + def get_weight(self, rarity: int, pity: int) -> int: + if rarity == 4: + return lerp(pity, self.weight4) + elif rarity == 5: + return lerp(pity, self.weight5) + else: + raise GachaIllegalArgument + + def has_epitomized(self): + return self.banner_type == BannerType.WEAPON + + def get_event_chance(self, rarity: int) -> int: + if rarity == 4: + return self.event_chance4 + elif rarity == 5: + return self.event_chance5 + elif self.event_chance >= -1: + return self.event_chance + else: + raise GachaIllegalArgument + + def get_pool_balance_weight(self, rarity: int, pity: int) -> int: + if rarity == 4: + return lerp(pity, self.pool_balance_weights4) + elif rarity == 5: + return lerp(pity, self.pool_balance_weights5) + else: + raise GachaIllegalArgument diff --git a/modules/gacha/error.py b/modules/gacha/error.py new file mode 100644 index 00000000..1e9cfa1e --- /dev/null +++ b/modules/gacha/error.py @@ -0,0 +1,14 @@ +class GachaException(Exception): + pass + + +class GachaInvalidTimes(GachaException): + pass + + +class GachaIllegalArgument(GachaException): + pass + + +class BannerNotFound(GachaException): + pass diff --git a/modules/gacha/player/banner.py b/modules/gacha/player/banner.py new file mode 100644 index 00000000..9e54a6f9 --- /dev/null +++ b/modules/gacha/player/banner.py @@ -0,0 +1,78 @@ +from pydantic import BaseModel + +from modules.gacha.error import GachaIllegalArgument + + +class PlayerGachaBannerInfo(BaseModel): + """玩家当前抽卡统计信息""" + + pity5: int = 0 + pity4: int = 0 + pity4_pool1: int = 0 + pity4_pool2: int = 0 + pity5_pool1: int = 0 + pity5_pool2: int = 0 + wish_item_id: int = 0 + failed_chosen_item_pulls: int = 0 + failed_featured4_item_pulls: int = 0 + failed_featured_item_pulls: int = 0 + total_pulls: int = 0 + + def inc_pity_all(self): + self.pity5 += 1 + self.pity4 += 1 + self.pity4_pool1 += 1 + self.pity4_pool2 += 1 + self.pity5_pool1 += 1 + self.pity5_pool2 += 1 + + def get_failed_featured_item_pulls(self, rarity: int) -> int: + if rarity == 4: + return self.failed_featured4_item_pulls + elif rarity == 5: + return self.failed_featured_item_pulls + else: + raise GachaIllegalArgument + + def set_failed_featured_item_pulls(self, rarity: int, amount: int): + if rarity == 4: + self.failed_featured4_item_pulls = amount + elif rarity == 5: + self.failed_featured_item_pulls = amount + else: + raise GachaIllegalArgument + + def add_failed_featured_item_pulls(self, rarity: int, amount: int): + if rarity == 4: + self.failed_featured4_item_pulls += amount + elif rarity == 5: + self.failed_featured_item_pulls += amount + else: + raise GachaIllegalArgument + + def get_pity_pool(self, rarity: int, param: int) -> int: + if rarity == 4: + return self.pity4_pool1 if param == 1 else self.pity4_pool2 + elif rarity == 5: + return self.pity5_pool1 if param == 1 else self.pity5_pool2 + raise GachaIllegalArgument + + def set_pity_pool(self, rarity: int, pool: int, amount: int): + if rarity == 4: + if pool == 1: + self.pity4_pool1 = amount + else: + self.pity4_pool2 = amount + elif rarity == 5: + if pool == 1: + self.pity5_pool1 = amount + else: + self.pity5_pool2 = amount + else: + raise GachaIllegalArgument + + def add_failed_chosen_item_pulls(self, amount: int): + self.failed_chosen_item_pulls += amount + + def add_total_pulls(self, times: int): + self.total_pulls += times diff --git a/modules/gacha/player/info.py b/modules/gacha/player/info.py new file mode 100644 index 00000000..03962567 --- /dev/null +++ b/modules/gacha/player/info.py @@ -0,0 +1,30 @@ +from typing import Optional + +from pydantic import BaseModel + +from modules.gacha.banner import GachaBanner, BannerType +from modules.gacha.player.banner import PlayerGachaBannerInfo + + +class PlayerGachaInfo(BaseModel): + """玩家抽卡全部信息""" + + standard_banner: Optional[PlayerGachaBannerInfo] = None + event_weapon_banner: Optional[PlayerGachaBannerInfo] = None + event_character_banner: Optional[PlayerGachaBannerInfo] = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.standard_banner is None: + self.standard_banner = PlayerGachaBannerInfo() + if self.event_weapon_banner is None: + self.event_weapon_banner = PlayerGachaBannerInfo() + if self.event_character_banner is None: + self.event_character_banner = PlayerGachaBannerInfo() + + def get_banner_info(self, banner: GachaBanner) -> PlayerGachaBannerInfo: + if banner.banner_type == BannerType.EVENT: + return self.event_character_banner + elif banner.banner_type == BannerType.WEAPON: + return self.event_weapon_banner + return self.standard_banner diff --git a/modules/gacha/pool.py b/modules/gacha/pool.py new file mode 100644 index 00000000..5e389a7a --- /dev/null +++ b/modules/gacha/pool.py @@ -0,0 +1,27 @@ +from typing import List + +from modules.gacha.banner import GachaBanner +from modules.gacha.utils import set_subtract + + +class BannerPool: + rate_up_items5: List[int] = [] + fallback_items5_pool1: List[int] = [] + fallback_items5_pool2: List[int] = [] + rate_up_items4: List[int] = [] + fallback_items4_pool1: List[int] = [] + fallback_items4_pool2: List[int] = [] + + def __init__(self, banner: GachaBanner): + self.rate_up_items4 = banner.rate_up_items4 + self.rate_up_items5 = banner.rate_up_items5 + self.fallback_items5_pool1 = banner.fallback_items5_pool1 + self.fallback_items5_pool2 = banner.fallback_items5_pool2 + self.fallback_items4_pool1 = banner.fallback_items4_pool1 + self.fallback_items4_pool2 = banner.fallback_items4_pool2 + + if banner.auto_strip_rate_up_from_fallback: # 把UP四星从非UP四星排除 + self.fallback_items5_pool1 = set_subtract(banner.fallback_items5_pool1, banner.rate_up_items5) + self.fallback_items5_pool2 = set_subtract(banner.fallback_items5_pool2, banner.rate_up_items5) + self.fallback_items4_pool1 = set_subtract(banner.fallback_items4_pool1, banner.rate_up_items4) + self.fallback_items4_pool2 = set_subtract(banner.fallback_items4_pool2, banner.rate_up_items4) diff --git a/modules/gacha/system.py b/modules/gacha/system.py new file mode 100644 index 00000000..3ffe96cd --- /dev/null +++ b/modules/gacha/system.py @@ -0,0 +1,156 @@ +import secrets +from typing import Tuple, List + +from modules.gacha.banner import GachaBanner +from modules.gacha.error import GachaInvalidTimes, GachaIllegalArgument +from modules.gacha.player.info import PlayerGachaBannerInfo +from modules.gacha.player.info import PlayerGachaInfo +from modules.gacha.pool import BannerPool + + +class BannerSystem: + fallback_items5_pool2_default: Tuple[int] = (11501, 11502, 12501, 12502, 13502, 13505, 14501, 14502, 15501, 15502) + fallback_items4_pool2_default: Tuple[int] = ( + 11401, + 11402, + 11403, + 11405, + 12401, + 12402, + 12403, + 12405, + 13401, + 13407, + 14401, + 14402, + 14403, + 14409, + 15401, + 15402, + 15403, + 15405, + ) + + def do_pulls(self, player_gacha_info: PlayerGachaInfo, banner: GachaBanner, times: int) -> List[int]: + item_list: List[int] = [] + if times not in (10, 1): + raise GachaInvalidTimes + + gacha_info = player_gacha_info.get_banner_info(banner) + gacha_info.add_total_pulls(times) + pools = BannerPool(banner) + for _ in range(times): + item_id = self.do_pull(banner, gacha_info, pools) + item_list.append(item_id) + return item_list + + def do_pull(self, banner: GachaBanner, gacha_info: PlayerGachaBannerInfo, pools: BannerPool) -> int: + gacha_info.inc_pity_all() + # 对玩家卡池信息的计数全部加1,方便计算 + # 就这么说吧,如果你加之前比已经四星9发没出,那么这个能让你下次权重必定让你出四星的角色 + # 而不是使用 if gacha_info.pity4 + 1 >= 10 的形式计算 + weights = [banner.get_weight(5, gacha_info.pity5), banner.get_weight(4, gacha_info.pity4), 10000] + leval_won = 5 - self.draw_roulette(weights, 10000) + # 根据权重信息获得当前所抽到的星级 + if leval_won == 5: + # print(f"已经获得五星,当前五星权重为{weights[0]}") + gacha_info.pity5 = 0 + return self.do_rare_pull( + pools.rate_up_items5, pools.fallback_items5_pool1, pools.fallback_items5_pool2, 5, banner, gacha_info + ) + elif leval_won == 4: + gacha_info.pity4 = 0 + return self.do_rare_pull( + pools.rate_up_items4, pools.fallback_items4_pool1, pools.fallback_items4_pool2, 4, banner, gacha_info + ) + else: + return self.get_random(banner.fallback_items3) + + @staticmethod + def draw_roulette(weights, cutoff: int) -> int: + total = 0 + for weight in weights: + if weight < 0: + raise GachaIllegalArgument("Weights must be non-negative!") + total += weight + secrets_random = secrets.SystemRandom() + roll = int(secrets_random.random() * min(total, cutoff)) + sub_total = 0 + for index, value in enumerate(weights): + sub_total += value + if roll < sub_total: + return index + return 0 + + def do_rare_pull( + self, + featured: List[int], + fallback1: List[int], + fallback2: List[int], + rarity: int, + banner: GachaBanner, + gacha_info: PlayerGachaBannerInfo, + ) -> int: + # 以下是防止点炒饭 + epitomized = (banner.has_epitomized()) and (rarity == 5) and (gacha_info.wish_item_id != 0) # 判断定轨信息是否正确 + pity_epitomized = gacha_info.failed_chosen_item_pulls >= banner.wish_max_progress # 判断定轨值 + pity_featured = gacha_info.get_failed_featured_item_pulls(rarity) >= 1 # 通过UP值判断当前是否为UP + roll_featured = self.random_range(1, 100) <= banner.get_event_chance(rarity) # 随机判断当前是否为UP + pull_featured = pity_featured or roll_featured # 获得最终是否为 UP + + if epitomized and pity_epitomized: # 给武器用的定轨代码 + gacha_info.set_failed_featured_item_pulls(rarity, 0) + item_id = gacha_info.wish_item_id + elif pull_featured and featured: # 是UP角色 + gacha_info.set_failed_featured_item_pulls(rarity, 0) + item_id = self.get_random(featured) + else: # 寄 + gacha_info.add_failed_featured_item_pulls(rarity, 1) + item_id = self.do_fallback_rare_pull(fallback1, fallback2, rarity, banner, gacha_info) + if epitomized: + if item_id == gacha_info.wish_item_id: # 判断当前UP是否为定轨的UP + gacha_info.failed_chosen_item_pulls = 0 # 是的话清除定轨 + else: + gacha_info.add_failed_chosen_item_pulls(1) + return item_id + + def do_fallback_rare_pull( + self, + fallback1: List[int], + fallback2: List[int], + rarity: int, + banner: GachaBanner, + gacha_info: PlayerGachaBannerInfo, + ) -> int: + if len(fallback1) < 1: + if len(fallback2) < 1: + return self.get_random( + self.fallback_items5_pool2_default if rarity == 5 else self.fallback_items4_pool2_default + ) + else: + return self.get_random(fallback2) + elif len(fallback2) < 1: + return self.get_random(fallback1) + else: + pity_pool1 = banner.get_pool_balance_weight(rarity, gacha_info.get_pity_pool(rarity, 1)) + pity_pool2 = banner.get_pool_balance_weight(rarity, gacha_info.get_pity_pool(rarity, 2)) + if pity_pool1 >= pity_pool2: + chosen_pool = 1 + self.draw_roulette((pity_pool1, pity_pool2), 10000) + else: + chosen_pool = 2 - self.draw_roulette((pity_pool2, pity_pool1), 10000) + if chosen_pool == 1: + gacha_info.set_pity_pool(rarity, 1, 0) + return self.get_random(fallback1) + gacha_info.set_pity_pool(rarity, 2, 0) + return self.get_random(fallback2) + + @staticmethod + def get_random(items) -> int: + secrets_random = secrets.SystemRandom() + roll = int(secrets_random.random() * len(items)) + return items[roll] + + @staticmethod + def random_range(_mix: int, _max: int) -> int: + secrets_random = secrets.SystemRandom() + return int(secrets_random.uniform(_mix, _max)) diff --git a/modules/gacha/utils.py b/modules/gacha/utils.py new file mode 100644 index 00000000..e9558f8c --- /dev/null +++ b/modules/gacha/utils.py @@ -0,0 +1,26 @@ +import contextlib +from typing import List + + +def lerp(x: int, x_y_array) -> int: + with contextlib.suppress(KeyError, IndexError): + if x <= x_y_array[0][0]: + return x_y_array[0][1] + elif x >= x_y_array[-1][0]: + return x_y_array[-1][1] + for index, _ in enumerate(x_y_array): + if x == x_y_array[index + 1][0]: + return x_y_array[index + 1][1] + if x < x_y_array[index + 1][0]: + position = x - x_y_array[index][0] + full_dist = x_y_array[index + 1][0] - x_y_array[index][0] + if full_dist == 0: + return position + prev_value = x_y_array[index][1] + full_delta = x_y_array[index + 1][1] - prev_value + return int(prev_value + ((position * full_delta) / full_dist)) + return 0 + + +def set_subtract(minuend: List[int], subtrahend: List[int]) -> List[int]: + return [i for i in minuend if i not in subtrahend]