LifeRPG_v2.0/modern/backend/gamification.py
TLimoges33 7fe4ae5365
🧙‍♂️ Transform LifeRPG into The Wizard's Grimoire - Production-Ready Application
 Major Features Added:
- Complete magical theming and rebranding from LifeRPG to The Wizard's Grimoire
- Production-grade React frontend with Tailwind CSS v4 and magical aesthetics
- Comprehensive analytics dashboard with Recharts integration (ScryingPortal)
- Push notifications system with PWA service worker support
- Drag & drop functionality using @dnd-kit for habit reordering
- Social features with friends system and leaderboards
- Performance optimization tools and monitoring
- Mobile app enhancement with PWA installation support

🏗️ Technical Infrastructure:
- Advanced service worker with offline support and background sync
- Zustand state management for scalable application state
- Production-ready UI component system with enhanced Button, Card, Input
- Progressive Web App (PWA) with manifest and app installation
- FastAPI backend with comprehensive API endpoints
- Docker containerization and CI/CD pipeline setup

📱 Progressive Web App Features:
- Offline functionality with intelligent caching
- Push notification support for habit reminders
- App installation on mobile and desktop platforms
- Background sync for offline data management
- Performance monitoring and optimization tools

🎨 User Experience:
- Magical wizard/grimoire theming throughout application
- Responsive design optimized for all device sizes
- Drag & drop habit management with smooth animations
- Interactive analytics with multiple chart types
- Social connectivity with friends and competitive features
- Comprehensive notification and performance settings

🔧 Developer Experience:
- Modern development stack with Vite and React
- Comprehensive testing setup and CI/CD pipelines
- Code quality tools with pre-commit hooks
- Docker development environment
- Detailed documentation and implementation guides

This represents a complete transformation from prototype to production-ready application with enterprise-grade features and magical user experience.
2025-08-30 17:32:42 +00:00

402 lines
13 KiB
Python

"""
Gamification engine for LifeRPG - XP, levels, achievements, and streaks.
"""
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import func
import models
import json
# XP and Level Configuration
XP_BASE = 100 # Base XP needed for level 2
XP_MULTIPLIER = 1.2 # Each level requires 20% more XP
MAX_LEVEL = 100
# Achievement Definitions
ACHIEVEMENT_DEFINITIONS = {
"first_habit": {
"name": "First Steps",
"description": "Complete your first habit",
"xp_reward": 50,
"icon": "🌱"
},
"streak_7": {
"name": "Week Warrior",
"description": "Maintain a 7-day streak",
"xp_reward": 100,
"icon": "🔥"
},
"streak_30": {
"name": "Monthly Master",
"description": "Maintain a 30-day streak",
"xp_reward": 500,
"icon": "💪"
},
"streak_100": {
"name": "Century Champion",
"description": "Maintain a 100-day streak",
"xp_reward": 2000,
"icon": "👑"
},
"habit_count_10": {
"name": "Habit Builder",
"description": "Create 10 habits",
"xp_reward": 200,
"icon": "🏗️"
},
"habit_count_50": {
"name": "Routine Master",
"description": "Create 50 habits",
"xp_reward": 1000,
"icon": ""
},
"xp_1000": {
"name": "Experience Gained",
"description": "Earn 1,000 XP",
"xp_reward": 0,
"icon": ""
},
"level_10": {
"name": "Rising Star",
"description": "Reach level 10",
"xp_reward": 500,
"icon": "🌟"
},
"level_25": {
"name": "Veteran Player",
"description": "Reach level 25",
"xp_reward": 1500,
"icon": "🎖️"
},
"perfect_week": {
"name": "Perfect Week",
"description": "Complete all active habits for 7 consecutive days",
"xp_reward": 300,
"icon": "💎"
}
}
def calculate_level_from_xp(total_xp: int) -> int:
"""Calculate level based on total XP."""
if total_xp < XP_BASE:
return 1
level = 1
xp_needed = XP_BASE
remaining_xp = total_xp
while remaining_xp >= xp_needed and level < MAX_LEVEL:
remaining_xp -= xp_needed
level += 1
xp_needed = int(xp_needed * XP_MULTIPLIER)
return level
def calculate_xp_for_level(level: int) -> int:
"""Calculate total XP needed to reach a given level."""
if level <= 1:
return 0
total_xp = 0
xp_needed = XP_BASE
for _ in range(2, level + 1):
total_xp += xp_needed
xp_needed = int(xp_needed * XP_MULTIPLIER)
return total_xp
def calculate_xp_for_next_level(current_xp: int) -> int:
"""Calculate XP needed for the next level."""
current_level = calculate_level_from_xp(current_xp)
if current_level >= MAX_LEVEL:
return 0
next_level_xp = calculate_xp_for_level(current_level + 1)
return next_level_xp - current_xp
def get_user_stats(db: Session, user_id: int) -> Dict:
"""Get comprehensive user gamification stats."""
# Get user's total XP from profile
xp_profile = db.query(models.Profile).filter(
models.Profile.user_id == user_id,
models.Profile.key == "total_xp"
).first()
total_xp = int(xp_profile.value) if xp_profile and xp_profile.value else 0
current_level = calculate_level_from_xp(total_xp)
xp_for_current_level = calculate_xp_for_level(current_level)
xp_for_next_level = calculate_xp_for_level(current_level + 1) if current_level < MAX_LEVEL else 0
xp_progress = total_xp - xp_for_current_level
xp_needed = xp_for_next_level - xp_for_current_level if current_level < MAX_LEVEL else 0
# Get habit stats
total_habits = db.query(models.Habit).filter(models.Habit.user_id == user_id).count()
active_habits = db.query(models.Habit).filter(
models.Habit.user_id == user_id,
models.Habit.status == "active"
).count()
# Get total completions
total_completions = db.query(models.Log).filter(
models.Log.user_id == user_id,
models.Log.action == "complete"
).count()
# Calculate current streak (simplified - longest consecutive days with any habit completion)
current_streak = calculate_current_streak(db, user_id)
longest_streak = calculate_longest_streak(db, user_id)
# Get achievements
achievements = db.query(models.Achievement).filter(
models.Achievement.user_id == user_id
).all()
return {
"total_xp": total_xp,
"current_level": current_level,
"xp_progress": xp_progress,
"xp_needed": xp_needed,
"xp_percentage": int((xp_progress / xp_needed * 100)) if xp_needed > 0 else 100,
"total_habits": total_habits,
"active_habits": active_habits,
"total_completions": total_completions,
"current_streak": current_streak,
"longest_streak": longest_streak,
"achievements_count": len(achievements),
"achievements": [
{
"id": a.id,
"name": a.name,
"description": a.description,
"earned_at": a.earned_at.isoformat() if a.earned_at else None
}
for a in achievements
]
}
def calculate_current_streak(db: Session, user_id: int) -> int:
"""Calculate user's current consecutive day streak."""
# Get recent completions, grouped by date
recent_logs = db.query(
func.date(models.Log.timestamp).label('log_date')
).filter(
models.Log.user_id == user_id,
models.Log.action == "complete",
models.Log.timestamp >= datetime.now() - timedelta(days=365)
).group_by(
func.date(models.Log.timestamp)
).order_by(
func.date(models.Log.timestamp).desc()
).all()
if not recent_logs:
return 0
# Check for consecutive days starting from today
today = datetime.now().date()
current_streak = 0
check_date = today
for log in recent_logs:
if log.log_date == check_date:
current_streak += 1
check_date = check_date - timedelta(days=1)
elif log.log_date == check_date - timedelta(days=1):
# Allow for today not having completions yet
current_streak += 1
check_date = log.log_date - timedelta(days=1)
else:
break
return current_streak
def calculate_longest_streak(db: Session, user_id: int) -> int:
"""Calculate user's longest ever consecutive day streak."""
# Get all completion dates
logs = db.query(
func.date(models.Log.timestamp).label('log_date')
).filter(
models.Log.user_id == user_id,
models.Log.action == "complete"
).group_by(
func.date(models.Log.timestamp)
).order_by(
func.date(models.Log.timestamp)
).all()
if not logs:
return 0
max_streak = 1
current_streak = 1
for i in range(1, len(logs)):
prev_date = logs[i-1].log_date
curr_date = logs[i].log_date
if curr_date == prev_date + timedelta(days=1):
current_streak += 1
max_streak = max(max_streak, current_streak)
else:
current_streak = 1
return max_streak
def award_xp(db: Session, user_id: int, xp_amount: int, source: str = "habit_completion") -> Dict:
"""Award XP to a user and check for level-ups and achievements."""
# Get current XP
xp_profile = db.query(models.Profile).filter(
models.Profile.user_id == user_id,
models.Profile.key == "total_xp"
).first()
old_xp = int(xp_profile.value) if xp_profile and xp_profile.value else 0
new_xp = old_xp + xp_amount
old_level = calculate_level_from_xp(old_xp)
new_level = calculate_level_from_xp(new_xp)
# Update XP in profile
if xp_profile:
xp_profile.value = str(new_xp)
else:
xp_profile = models.Profile(user_id=user_id, key="total_xp", value=str(new_xp))
db.add(xp_profile)
# Check for level-up achievements
level_up = new_level > old_level
new_achievements = []
if level_up:
# Check level-based achievements
for achievement_key in ["level_10", "level_25"]:
if achievement_key not in [a["name"] for a in new_achievements]:
required_level = int(achievement_key.split("_")[1])
if new_level >= required_level and old_level < required_level:
achievement = award_achievement(db, user_id, achievement_key)
if achievement:
new_achievements.append(achievement)
# Check XP-based achievements
if new_xp >= 1000 and old_xp < 1000:
achievement = award_achievement(db, user_id, "xp_1000")
if achievement:
new_achievements.append(achievement)
db.commit()
return {
"xp_awarded": xp_amount,
"total_xp": new_xp,
"old_level": old_level,
"new_level": new_level,
"level_up": level_up,
"new_achievements": new_achievements,
"source": source
}
def award_achievement(db: Session, user_id: int, achievement_key: str) -> Optional[Dict]:
"""Award an achievement to a user if they don't already have it."""
# Check if user already has this achievement
existing = db.query(models.Achievement).filter(
models.Achievement.user_id == user_id,
models.Achievement.name == achievement_key
).first()
if existing:
return None
# Get achievement definition
achievement_def = ACHIEVEMENT_DEFINITIONS.get(achievement_key)
if not achievement_def:
return None
# Create achievement
achievement = models.Achievement(
user_id=user_id,
name=achievement_key,
description=f"{achievement_def['name']}: {achievement_def['description']}",
earned_at=datetime.now()
)
db.add(achievement)
# Award XP bonus if specified
if achievement_def.get("xp_reward", 0) > 0:
award_xp(db, user_id, achievement_def["xp_reward"], f"achievement_{achievement_key}")
return {
"key": achievement_key,
"name": achievement_def["name"],
"description": achievement_def["description"],
"xp_reward": achievement_def.get("xp_reward", 0),
"icon": achievement_def.get("icon", "🏆")
}
def check_habit_achievements(db: Session, user_id: int) -> List[Dict]:
"""Check and award habit-related achievements."""
new_achievements = []
# Check habit count achievements
total_habits = db.query(models.Habit).filter(models.Habit.user_id == user_id).count()
if total_habits >= 10:
achievement = award_achievement(db, user_id, "habit_count_10")
if achievement:
new_achievements.append(achievement)
if total_habits >= 50:
achievement = award_achievement(db, user_id, "habit_count_50")
if achievement:
new_achievements.append(achievement)
# Check first habit achievement
if total_habits >= 1:
achievement = award_achievement(db, user_id, "first_habit")
if achievement:
new_achievements.append(achievement)
# Check streak achievements
current_streak = calculate_current_streak(db, user_id)
if current_streak >= 7:
achievement = award_achievement(db, user_id, "streak_7")
if achievement:
new_achievements.append(achievement)
if current_streak >= 30:
achievement = award_achievement(db, user_id, "streak_30")
if achievement:
new_achievements.append(achievement)
if current_streak >= 100:
achievement = award_achievement(db, user_id, "streak_100")
if achievement:
new_achievements.append(achievement)
return new_achievements
def process_habit_completion(db: Session, user_id: int, habit_id: int) -> Dict:
"""Process a habit completion - award XP and check achievements."""
habit = db.query(models.Habit).filter(
models.Habit.id == habit_id,
models.Habit.user_id == user_id
).first()
if not habit:
raise ValueError("Habit not found")
# Award XP based on habit difficulty/reward
xp_amount = habit.xp_reward or 10
xp_result = award_xp(db, user_id, xp_amount, "habit_completion")
# Check for new achievements
habit_achievements = check_habit_achievements(db, user_id)
# Combine achievement lists
all_achievements = xp_result.get("new_achievements", []) + habit_achievements
xp_result["new_achievements"] = all_achievements
return xp_result