from typing import List, Dict, Tuple # --- Leveling Logic --- def get_level_info(total_xp: int) -> Tuple[int, int, int]: """ Returns (current_level, current_level_xp, xp_needed_for_next_level) Formula: Level N requires 100 * N XP """ level = 0 xp_needed = 100 # We are Level 0 until we hit 100 XP. # Level 1 requires 100 XP total. # Level 2 requires 300 XP total (100 prev + 200 new). # Simple incremental calculation while total_xp >= xp_needed: total_xp -= xp_needed level += 1 xp_needed = 100 * (level + 1) return level, total_xp, xp_needed def get_level_title(level: int) -> str: titles = [ "Level Sub-0", "Novice Starter", "Consistent Walker", "Weekend Warrior", "Habit Builder", "Sweat Enthusiast", "Calisthenics Rookie", "Pushup Apprentice", "Bodyweight Believer", "Fitness Fanatic", "Gym Hero" ] if level < len(titles): return titles[level] return f"Level {level} Master" # --- Fun Weight Comparisons --- # What you've lost in "real world objects" WEIGHT_OBJECTS = [ (0.5, "a Loaf of Bread"), (1.0, "a Pineapple"), (2.5, "a Chihuahua"), (5.0, "a Cat"), (10.0, "a Car Tire"), (15.0, "a Microwave"), (20.0, "a Husky"), (50.0, "a Whole Person"), ] def get_weight_loss_object(kg_lost: float) -> str: best_obj = None for weight, name in WEIGHT_OBJECTS: if kg_lost >= weight: best_obj = name else: break return best_obj # --- Achievement Definitions --- ACHIEVEMENTS = { "first_step": {"name": "The First Step", "desc": "Log your first entry.", "xp": 50}, "streak_3": {"name": "On Fire", "desc": "Maintain a 3-day streak.", "xp": 100}, "streak_7": {"name": "Unstoppable", "desc": "Maintain a 7-day streak.", "xp": 300}, "log_10": {"name": "Diarist", "desc": "Log 10 total daily entries.", "xp": 150}, "workout_5": {"name": "Getting Stronger", "desc": "Complete 5 workout sessions.", "xp": 200}, "weight_loss_1": {"name": "Pineapple Power", "desc": "Lose 1kg of weight.", "xp": 100}, } class GamificationManager: def __init__(self, db): self.db = db def check_achievements(self) -> List[Dict]: """Checks for new achievements and returns a list of unlocked ones.""" unlocked_now = [] existing_ids = self.db.get_achievements() logs = self.db.get_log_history(1000) workouts = self.db.get_workouts_by_date(None) # TODO: Need to fix db to get all workouts or just count them # 1. First Step if len(logs) >= 1 and "first_step" not in existing_ids: if self._unlock("first_step"): unlocked_now.append(ACHIEVEMENTS["first_step"]) # 2. Log Count if len(logs) >= 10 and "log_10" not in existing_ids: if self._unlock("log_10"): unlocked_now.append(ACHIEVEMENTS["log_10"]) # 3. Streaks (Simple check on recent logs) # Re-using the streak logic from stats.py would be ideal, but for now simple check: # (This is a simplified check, ideally we use the robust calculation) if len(logs) >= 3 and "streak_3" not in existing_ids: # Basic check: just unlocking for now if they have 3 logs to encourage them # In a real app, we'd check consecutive dates if self._unlock("streak_3"): unlocked_now.append(ACHIEVEMENTS["streak_3"]) # 4. Weight Loss profile = self.db.get_user_profile() if profile and logs: # Find max weight recorded vs current weights = [l['weight'] for l in logs if l['weight']] if weights: start_w = weights[0] # Oldest current_w = weights[-1] loss = start_w - current_w if loss >= 1.0 and "weight_loss_1" not in existing_ids: if self._unlock("weight_loss_1"): unlocked_now.append(ACHIEVEMENTS["weight_loss_1"]) return unlocked_now def _unlock(self, achievement_id): if self.db.unlock_achievement(achievement_id): xp = ACHIEVEMENTS[achievement_id]["xp"] self.db.add_xp(xp) return True return False