Added new systems.

This commit is contained in:
2026-01-09 10:15:46 -06:00
parent 725b81494e
commit 267504e641
39 changed files with 4441 additions and 18 deletions

View File

@ -5,6 +5,18 @@ from app.models.bet import Bet, BetProposal, BetCategory, BetStatus, BetVisibili
from app.models.sport_event import SportEvent, SportType, EventStatus
from app.models.spread_bet import SpreadBet, SpreadBetStatus, TeamSide
from app.models.admin_settings import AdminSettings
from app.models.gamification import (
UserStats,
Achievement,
UserAchievement,
LootBox,
ActivityFeed,
DailyReward,
AchievementType,
LootBoxRarity,
LootBoxRewardType,
TIER_CONFIG,
)
__all__ = [
"User",
@ -26,4 +38,15 @@ __all__ = [
"SpreadBetStatus",
"TeamSide",
"AdminSettings",
# Gamification
"UserStats",
"Achievement",
"UserAchievement",
"LootBox",
"ActivityFeed",
"DailyReward",
"AchievementType",
"LootBoxRarity",
"LootBoxRewardType",
"TIER_CONFIG",
]

View File

@ -0,0 +1,216 @@
"""
Gamification models for H2H betting platform
Includes: Tiers, XP, Achievements, Loot Boxes, Streaks
"""
from sqlalchemy import String, DateTime, Enum, Float, Integer, ForeignKey, Boolean, Text, JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship
from datetime import datetime
import enum
from app.database import Base
class AchievementType(enum.Enum):
"""Types of achievements"""
FIRST_BET = "first_bet"
FIRST_WIN = "first_win"
WIN_STREAK_3 = "win_streak_3"
WIN_STREAK_5 = "win_streak_5"
WIN_STREAK_10 = "win_streak_10"
WHALE_BET = "whale_bet" # Single bet over $1000
HIGH_ROLLER = "high_roller" # Total wagered over $10k
CONSISTENT = "consistent" # Bet every day for a week
UNDERDOG = "underdog" # Win 5 underdog bets
SHARPSHOOTER = "sharpshooter" # 70%+ win rate with 20+ bets
VETERAN = "veteran" # 100 total bets
LEGEND = "legend" # 500 total bets
PROFIT_MASTER = "profit_master" # $5000+ lifetime profit
COMEBACK_KING = "comeback_king" # Win after 5 loss streak
EARLY_BIRD = "early_bird" # Bet on event 24h+ before start
SOCIAL_BUTTERFLY = "social_butterfly" # Bet against 10 different users
TIER_UP = "tier_up" # Reach a new tier
MAX_TIER = "max_tier" # Reach tier 10
class LootBoxRarity(enum.Enum):
"""Loot box rarities"""
COMMON = "common"
UNCOMMON = "uncommon"
RARE = "rare"
EPIC = "epic"
LEGENDARY = "legendary"
class LootBoxRewardType(enum.Enum):
"""Types of rewards from loot boxes"""
BONUS_CASH = "bonus_cash"
XP_BOOST = "xp_boost"
FEE_REDUCTION = "fee_reduction" # Temporary fee reduction
FREE_BET = "free_bet"
AVATAR_FRAME = "avatar_frame"
BADGE = "badge"
NOTHING = "nothing" # Bad luck!
# Tier configuration: tier -> (min_xp, house_fee_percent, name)
TIER_CONFIG = {
0: (0, 10.0, "Bronze I"),
1: (1000, 9.5, "Bronze II"),
2: (3000, 9.0, "Bronze III"),
3: (7000, 8.5, "Silver I"),
4: (15000, 8.0, "Silver II"),
5: (30000, 7.5, "Silver III"),
6: (60000, 7.0, "Gold I"),
7: (100000, 6.5, "Gold II"),
8: (175000, 6.0, "Gold III"),
9: (300000, 5.5, "Platinum"),
10: (500000, 5.0, "Diamond"),
}
class UserStats(Base):
"""Extended user statistics for gamification"""
__tablename__ = "user_stats"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), unique=True)
# XP and Tier
xp: Mapped[int] = mapped_column(Integer, default=0)
tier: Mapped[int] = mapped_column(Integer, default=0)
# Detailed Stats
total_wagered: Mapped[float] = mapped_column(Float, default=0.0)
total_won: Mapped[float] = mapped_column(Float, default=0.0)
total_lost: Mapped[float] = mapped_column(Float, default=0.0)
net_profit: Mapped[float] = mapped_column(Float, default=0.0)
biggest_win: Mapped[float] = mapped_column(Float, default=0.0)
biggest_bet: Mapped[float] = mapped_column(Float, default=0.0)
# Streaks
current_win_streak: Mapped[int] = mapped_column(Integer, default=0)
current_loss_streak: Mapped[int] = mapped_column(Integer, default=0)
best_win_streak: Mapped[int] = mapped_column(Integer, default=0)
worst_loss_streak: Mapped[int] = mapped_column(Integer, default=0)
# Activity
bets_today: Mapped[int] = mapped_column(Integer, default=0)
bets_this_week: Mapped[int] = mapped_column(Integer, default=0)
bets_this_month: Mapped[int] = mapped_column(Integer, default=0)
consecutive_days_betting: Mapped[int] = mapped_column(Integer, default=0)
last_bet_date: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# Opponents
unique_opponents: Mapped[int] = mapped_column(Integer, default=0)
# Timestamps
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Relationship
user: Mapped["User"] = relationship(back_populates="stats")
def get_house_fee(self) -> float:
"""Get the house fee percentage for this user's tier"""
return TIER_CONFIG.get(self.tier, TIER_CONFIG[0])[1]
def get_tier_name(self) -> str:
"""Get the display name for this user's tier"""
return TIER_CONFIG.get(self.tier, TIER_CONFIG[0])[2]
def xp_to_next_tier(self) -> int:
"""Get XP needed to reach the next tier"""
if self.tier >= 10:
return 0
next_tier_xp = TIER_CONFIG[self.tier + 1][0]
return max(0, next_tier_xp - self.xp)
def tier_progress_percent(self) -> float:
"""Get progress percentage to next tier"""
if self.tier >= 10:
return 100.0
current_tier_xp = TIER_CONFIG[self.tier][0]
next_tier_xp = TIER_CONFIG[self.tier + 1][0]
tier_xp_range = next_tier_xp - current_tier_xp
xp_into_tier = self.xp - current_tier_xp
return min(100.0, (xp_into_tier / tier_xp_range) * 100)
class Achievement(Base):
"""Achievement definitions"""
__tablename__ = "achievements"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
type: Mapped[AchievementType] = mapped_column(Enum(AchievementType), unique=True)
name: Mapped[str] = mapped_column(String(100))
description: Mapped[str] = mapped_column(String(500))
icon: Mapped[str] = mapped_column(String(50)) # Emoji or icon name
xp_reward: Mapped[int] = mapped_column(Integer, default=100)
rarity: Mapped[str] = mapped_column(String(20), default="common") # common, rare, epic, legendary
# Relationships
user_achievements: Mapped[list["UserAchievement"]] = relationship(back_populates="achievement")
class UserAchievement(Base):
"""Tracks which achievements users have earned"""
__tablename__ = "user_achievements"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
achievement_id: Mapped[int] = mapped_column(ForeignKey("achievements.id"))
earned_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
notified: Mapped[bool] = mapped_column(Boolean, default=False)
# Relationships
user: Mapped["User"] = relationship(back_populates="achievements")
achievement: Mapped["Achievement"] = relationship(back_populates="user_achievements")
class LootBox(Base):
"""Loot box inventory for users"""
__tablename__ = "loot_boxes"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
rarity: Mapped[LootBoxRarity] = mapped_column(Enum(LootBoxRarity))
source: Mapped[str] = mapped_column(String(50)) # How they got it: tier_up, achievement, daily, etc.
opened: Mapped[bool] = mapped_column(Boolean, default=False)
reward_type: Mapped[LootBoxRewardType | None] = mapped_column(Enum(LootBoxRewardType), nullable=True)
reward_value: Mapped[str | None] = mapped_column(String(100), nullable=True) # JSON or simple value
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
opened_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
# Relationship
user: Mapped["User"] = relationship(back_populates="loot_boxes")
class ActivityFeed(Base):
"""Global activity feed for recent bets, wins, etc."""
__tablename__ = "activity_feed"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
activity_type: Mapped[str] = mapped_column(String(50)) # bet_placed, bet_won, achievement, tier_up, whale_bet
message: Mapped[str] = mapped_column(String(500))
extra_data: Mapped[str | None] = mapped_column(Text, nullable=True) # JSON for extra data
amount: Mapped[float | None] = mapped_column(Float, nullable=True)
is_public: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationship
user: Mapped["User"] = relationship(back_populates="activities")
class DailyReward(Base):
"""Daily login rewards"""
__tablename__ = "daily_rewards"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
day_streak: Mapped[int] = mapped_column(Integer, default=1)
reward_type: Mapped[str] = mapped_column(String(50)) # xp, loot_box, bonus_cash
reward_value: Mapped[str] = mapped_column(String(100))
claimed_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# Relationship
user: Mapped["User"] = relationship(back_populates="daily_rewards")

View File

@ -44,3 +44,10 @@ class User(Base):
transactions: Mapped[list["Transaction"]] = relationship(back_populates="user")
created_spread_bets: Mapped[list["SpreadBet"]] = relationship(back_populates="creator", foreign_keys="SpreadBet.creator_id")
taken_spread_bets: Mapped[list["SpreadBet"]] = relationship(back_populates="taker", foreign_keys="SpreadBet.taker_id")
# Gamification relationships
stats: Mapped["UserStats"] = relationship(back_populates="user", uselist=False)
achievements: Mapped[list["UserAchievement"]] = relationship(back_populates="user")
loot_boxes: Mapped[list["LootBox"]] = relationship(back_populates="user")
activities: Mapped[list["ActivityFeed"]] = relationship(back_populates="user")
daily_rewards: Mapped[list["DailyReward"]] = relationship(back_populates="user")