mirror of
https://github.com/PaiGramTeam/PaiGram.git
synced 2024-11-16 04:35:49 +00:00
✨ 添加新抽卡模拟系统
This commit is contained in:
parent
24dbecd629
commit
242826121d
75
modules/gacha/banner.py
Normal file
75
modules/gacha/banner.py
Normal file
@ -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
|
14
modules/gacha/error.py
Normal file
14
modules/gacha/error.py
Normal file
@ -0,0 +1,14 @@
|
||||
class GachaException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class GachaInvalidTimes(GachaException):
|
||||
pass
|
||||
|
||||
|
||||
class GachaIllegalArgument(GachaException):
|
||||
pass
|
||||
|
||||
|
||||
class BannerNotFound(GachaException):
|
||||
pass
|
78
modules/gacha/player/banner.py
Normal file
78
modules/gacha/player/banner.py
Normal file
@ -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
|
30
modules/gacha/player/info.py
Normal file
30
modules/gacha/player/info.py
Normal file
@ -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
|
27
modules/gacha/pool.py
Normal file
27
modules/gacha/pool.py
Normal file
@ -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)
|
156
modules/gacha/system.py
Normal file
156
modules/gacha/system.py
Normal file
@ -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))
|
26
modules/gacha/utils.py
Normal file
26
modules/gacha/utils.py
Normal file
@ -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]
|
Loading…
Reference in New Issue
Block a user