accurate banner pools: standard, pickup, limited, fes with weighted rate-up
This commit is contained in:
1 parent
5e67782e58
commit
ed95c7ac30
2 files changed
+92
-26
No files matched your search
+80
-12
@@ -99,29 +99,97 @@ class StudentDB:
|
||||
if rarity in self.by_rarity:
|
||||
self.by_rarity[rarity].append(s)
|
||||
|
||||
# Pre-build standard pool (IsLimited == 0 only) and full pool
|
||||
self._std_pool: dict[int, list[dict]] = {1: [], 2: [], 3: []}
|
||||
self._limited_pool: list[dict] = [] # all IsLimited == 1 chars
|
||||
for s in self.students:
|
||||
limit = s.get("IsLimited") or 0
|
||||
if limit >= 2:
|
||||
continue
|
||||
r = s["StarGrade"]
|
||||
if limit == 0 and r in self._std_pool:
|
||||
self._std_pool[r].append(s)
|
||||
elif limit == 1:
|
||||
self._limited_pool.append(s)
|
||||
|
||||
self._loaded = True
|
||||
log.info(
|
||||
"Loaded %d students (1*: %d, 2*: %d, 3*: %d) — %d welfare excluded",
|
||||
"Loaded %d students (std: %d, limited: %d, welfare: %d)",
|
||||
len(self.students),
|
||||
len(self.by_rarity[1]),
|
||||
len(self.by_rarity[2]),
|
||||
len(self.by_rarity[3]),
|
||||
sum(len(v) for v in self._std_pool.values()),
|
||||
len(self._limited_pool),
|
||||
excluded,
|
||||
)
|
||||
|
||||
def random_by_rarity(self, rarity: int, exclude_limited: bool = False) -> dict:
|
||||
"""Pick a random student of the given base rarity.
|
||||
def get(self, student_id: int) -> Optional[dict]:
|
||||
return self.by_id.get(student_id)
|
||||
|
||||
If exclude_limited is True, only returns standard-pool characters
|
||||
(IsLimited == 0). Used by Regular Recruitment to exclude banner-only units.
|
||||
def get_by_name(self, name: str) -> Optional[dict]:
|
||||
return self.by_name.get(name.lower())
|
||||
|
||||
def build_pool(self, banner: Optional[dict]) -> dict[int, list[dict]]:
|
||||
"""Build the effective character pool for a banner.
|
||||
|
||||
Returns {1: [...], 2: [...], 3: [...]} with characters pullable on this banner.
|
||||
|
||||
- Regular Recruitment (banner=None): standard pool only
|
||||
- PickupGacha / LimitedGacha: standard pool + rate-up students
|
||||
- FesGacha: standard pool + all limiteds + rate-ups
|
||||
"""
|
||||
pool: dict[int, list[dict]] = {
|
||||
r: list(self._std_pool[r]) for r in (1, 2, 3)
|
||||
}
|
||||
|
||||
if banner is None:
|
||||
return pool
|
||||
|
||||
gtype = banner.get("gachaType", "")
|
||||
rateup_names = [n.lower() for n in banner.get("rateups", [])]
|
||||
|
||||
# For Fes banners, add ALL past limited characters to the pool
|
||||
if gtype == "FesGacha":
|
||||
for s in self._limited_pool:
|
||||
r = s["StarGrade"]
|
||||
if r in pool:
|
||||
pool[r].append(s)
|
||||
|
||||
# Add rate-up students (they might already be in the pool, but we need
|
||||
# to identify them for weighted selection later)
|
||||
for name in rateup_names:
|
||||
s = self.get_by_name(name)
|
||||
if s and (s.get("IsLimited") or 0) < 2:
|
||||
r = s["StarGrade"]
|
||||
if r in pool and s not in pool[r]:
|
||||
pool[r].append(s)
|
||||
|
||||
return pool
|
||||
|
||||
def weighted_pick(self, pool: list[dict], rateup_names: list[str], rarity: int) -> dict:
|
||||
"""Pick a student from a pool with rate-up weighting.
|
||||
|
||||
For 3★ rarity, rate-up students each get ~23% weight (matching the
|
||||
real game's 0.7% per rate-up out of 3% total). For 1-2★, uniform.
|
||||
"""
|
||||
pool = self.by_rarity.get(rarity, [])
|
||||
if exclude_limited:
|
||||
pool = [s for s in pool if (s.get("IsLimited") or 0) == 0]
|
||||
if not pool:
|
||||
pool = self.students
|
||||
return random.choice(self.students)
|
||||
|
||||
if rarity == 3 and rateup_names:
|
||||
# Weighted: rate-up chars get 3x weight each vs normal chars
|
||||
weights = []
|
||||
for s in pool:
|
||||
if s["Name"].lower() in rateup_names:
|
||||
weights.append(3.0)
|
||||
else:
|
||||
weights.append(1.0)
|
||||
return random.choices(pool, weights=weights, k=1)[0]
|
||||
|
||||
return random.choice(pool)
|
||||
|
||||
def random_by_rarity(self, rarity: int) -> dict:
|
||||
"""Pick any student of given rarity from the full pullable pool (fallback)."""
|
||||
pool = self.by_rarity.get(rarity, [])
|
||||
return random.choice(pool) if pool else random.choice(self.students)
|
||||
|
||||
def wiki_portrait(self, student: dict) -> str:
|
||||
"""Wiki face-portrait CDN URL for a student dict (252x204)."""
|
||||
return wiki_portrait_url(student)
|
||||
|
||||
+12
-14
@@ -230,8 +230,13 @@ class BlueArchiveGacha(commands.Cog):
|
||||
# "Opening envelope" teaser
|
||||
teaser = await ctx.reply("Opening recruitment envelope" + ("s..." if count > 1 else "..."))
|
||||
|
||||
# Determine pool scope: regular banner excludes limiteds
|
||||
is_regular = (banner_id is None or banner_id == "regular")
|
||||
# Build the banner-specific character pool
|
||||
if banner_id and banner_id != "regular" and banner:
|
||||
pool = db.build_pool(banner)
|
||||
rateup_names = [n.lower() for n in banner.get("rateups", [])]
|
||||
else:
|
||||
pool = db.build_pool(None) # regular recruitment
|
||||
rateup_names = []
|
||||
|
||||
# Perform pulls
|
||||
pulls = []
|
||||
@@ -242,18 +247,11 @@ class BlueArchiveGacha(commands.Cog):
|
||||
r = PULL10_RATES
|
||||
|
||||
rarity = roll_rarity(r)
|
||||
student = db.random_by_rarity(rarity, exclude_limited=is_regular)
|
||||
|
||||
# Check for rate-up weighting (simplified: 30% chance to hit rate-up if 3★)
|
||||
if banner and rarity == 3 and banner.get("rateups"):
|
||||
rateup_names = [n.lower() for n in banner["rateups"]]
|
||||
if random.random() < 0.30:
|
||||
# Try to find the rate-up student in our DB
|
||||
for rn in rateup_names:
|
||||
s = db.get_by_name(rn)
|
||||
if s and s["StarGrade"] == 3:
|
||||
student = s
|
||||
break
|
||||
rarity_pool = pool.get(rarity, [])
|
||||
if rarity_pool:
|
||||
student = db.weighted_pick(rarity_pool, rateup_names, rarity)
|
||||
else:
|
||||
student = db.random_by_rarity(rarity) # fallback
|
||||
|
||||
pulls.append(student)
|
||||
|
||||
|
||||
Reference in new issue
Block a user