Dbu-central-bot / app /services /xp_rules.py
cipher-attacks's picture
inline fuzzy + courses
d896946
Raw History Blame Contribute Delete
1.89 kB
"""The XP economy. Pure data + functions: easy to read, tune and test.
Design goals
* Consistency beats bursts (daily cap, active-days requirement for prizes).
* Referrals only pay in full when the invited student becomes a REAL active user.
* Nothing here can be farmed with a script: caps, idempotent events, delayed rewards.
"""
from typing import Optional
DAILY_CAP = 25 # max XP/day from the automatic sources below
CAPPED_KINDS = frozenset({"checkin", "download", "referral_join"})
PER_KIND_DAILY = {"download": 3, "referral_join": 5} # max events per day
REFERRAL_JOIN_XP = 5 # invitee joined the channel + passed the gate
REFERRAL_QUALIFIED_XP = 30 # invitee later proved active (see QUALIFY_* below)
REFERRAL_QUALIFIED_PER_WEEK = 5 # paid referrals counted per week
QUALIFY_MIN_ACTIVE_DAYS = 3
QUALIFY_WINDOW_DAYS = 14
VELOCITY_LIMIT_24H = 8 # more invitees than this in 24h from one inviter = suspicious
MIN_TRUST = 70
STREAK_MILESTONES = {7: 10, 14: 25, 30: 60, 60: 120}
LEVEL_THRESHOLDS = (0, 25, 75, 150, 275, 450, 700, 1050, 1550, 2300) # level n needs >= [n-1]
MAX_LEVEL = len(LEVEL_THRESHOLDS)
def checkin_xp(streak: int) -> int:
"""3 XP on day 1, +1 per extra streak day, up to 7."""
return 3 + min(max(streak - 1, 0), 4)
def level_for(points: int) -> tuple[int, Optional[int]]:
"""-> (level, XP still needed for the next level, or None at max level)."""
level = 1
for i, threshold in enumerate(LEVEL_THRESHOLDS, start=1):
if points >= threshold:
level = i
if level >= MAX_LEVEL:
return level, None
return level, LEVEL_THRESHOLDS[level] - points
def next_streak(last_active_iso: Optional[str], today_iso: str, current: int, yesterday_iso: str) -> int:
if last_active_iso == today_iso:
return current
if last_active_iso == yesterday_iso:
return current + 1
return 1