Added new systems.
This commit is contained in:
@ -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",
|
||||
]
|
||||
|
||||
216
backend/app/models/gamification.py
Normal file
216
backend/app/models/gamification.py
Normal 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")
|
||||
@ -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")
|
||||
|
||||
Reference in New Issue
Block a user