Spaces:
Sleeping
Sleeping
| """ | |
| gamification.py | |
| Handles XP, streaks, badges, and levels for LearnCraft. | |
| """ | |
| import json | |
| import os | |
| from datetime import date, timedelta | |
| GAMIFICATION_FILE = "gamification.json" | |
| # ββ XP values ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| XP_STUDY_SESSION = 10 | |
| XP_QUIZ_COMPLETE = 20 | |
| XP_PERFECT_SCORE = 50 | |
| XP_SCORE_ABOVE_80 = 30 | |
| XP_SCORE_ABOVE_60 = 15 | |
| XP_FLASHCARD_DECK = 10 | |
| XP_STREAK_BONUS = 5 # per day of streak | |
| # ββ Level thresholds βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| LEVELS = [ | |
| (0, "π± Seedling"), | |
| (50, "π Reader"), | |
| (150, "π Student"), | |
| (300, "π¬ Scholar"), | |
| (500, "π Expert"), | |
| (800, "π Master"), | |
| (1200, "π Genius"), | |
| ] | |
| # ββ Badge definitions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BADGES = { | |
| "first_quiz": {"name": "First Quiz", "icon": "π―", "desc": "Complete your first quiz"}, | |
| "perfect_score": {"name": "Perfect Score", "icon": "π―", "desc": "Score 100% on a quiz"}, | |
| "streak_3": {"name": "3-Day Streak", "icon": "π₯", "desc": "Study 3 days in a row"}, | |
| "streak_7": {"name": "Week Warrior", "icon": "β‘", "desc": "Study 7 days in a row"}, | |
| "streak_14": {"name": "Fortnight Hero", "icon": "ποΈ", "desc": "Study 14 days in a row"}, | |
| "topics_5": {"name": "Explorer", "icon": "πΊοΈ", "desc": "Study 5 different topics"}, | |
| "topics_10": {"name": "Polymath", "icon": "π§ ", "desc": "Study 10 different topics"}, | |
| "quizzes_10": {"name": "Quiz Master", "icon": "π§©", "desc": "Complete 10 quizzes"}, | |
| "quizzes_25": {"name": "Quiz Champion", "icon": "π ", "desc": "Complete 25 quizzes"}, | |
| "score_above_80": {"name": "High Achiever", "icon": "π", "desc": "Score above 80% on a quiz"}, | |
| "flashcards": {"name": "Card Shark", "icon": "π", "desc": "Complete a flashcard deck"}, | |
| "level_scholar": {"name": "Scholar", "icon": "π¬", "desc": "Reach Scholar level (300 XP)"}, | |
| "level_expert": {"name": "Expert", "icon": "π", "desc": "Reach Expert level (500 XP)"}, | |
| "level_master": {"name": "Master", "icon": "π", "desc": "Reach Master level (800 XP)"}, | |
| "notes_saver": {"name": "Note Taker", "icon": "π", "desc": "Save your first note"}, | |
| } | |
| def load_gamification() -> dict: | |
| if os.path.exists(GAMIFICATION_FILE): | |
| try: | |
| with open(GAMIFICATION_FILE, "r") as f: | |
| return json.load(f) | |
| except (json.JSONDecodeError, IOError): | |
| pass | |
| return { | |
| "xp": 0, | |
| "badges": [], | |
| "streak": 0, | |
| "last_study_date": None, | |
| "total_quizzes": 0, | |
| "study_dates": [], | |
| } | |
| def save_gamification(data: dict) -> None: | |
| try: | |
| with open(GAMIFICATION_FILE, "w") as f: | |
| json.dump(data, f, indent=2) | |
| except IOError: | |
| pass | |
| def get_level(xp: int) -> tuple: | |
| """Return (level_name, xp_for_next, xp_in_current_level, progress_pct).""" | |
| current_level = LEVELS[0] | |
| next_level = None | |
| for i, (threshold, name) in enumerate(LEVELS): | |
| if xp >= threshold: | |
| current_level = (threshold, name) | |
| next_level = LEVELS[i + 1] if i + 1 < len(LEVELS) else None | |
| if next_level: | |
| xp_start = current_level[0] | |
| xp_end = next_level[0] | |
| progress = (xp - xp_start) / (xp_end - xp_start) | |
| return current_level[1], next_level[1], xp_end - xp, round(progress * 100) | |
| return current_level[1], None, 0, 100 | |
| def update_streak(data: dict) -> dict: | |
| today = str(date.today()) | |
| yesterday = str(date.today() - timedelta(days=1)) | |
| last = data.get("last_study_date") | |
| study_dates = data.get("study_dates", []) | |
| if today not in study_dates: | |
| study_dates.append(today) | |
| data["study_dates"] = study_dates | |
| if last == today: | |
| pass # already counted today | |
| elif last == yesterday: | |
| data["streak"] = data.get("streak", 0) + 1 | |
| data["last_study_date"] = today | |
| else: | |
| data["streak"] = 1 | |
| data["last_study_date"] = today | |
| return data | |
| def award_xp(data: dict, amount: int, reason: str = "") -> tuple: | |
| """Add XP and return (new_data, xp_awarded, level_up_msg).""" | |
| old_xp = data.get("xp", 0) | |
| old_level = get_level(old_xp)[0] | |
| data["xp"] = old_xp + amount | |
| new_level = get_level(data["xp"])[0] | |
| level_up = new_level if new_level != old_level else None | |
| return data, amount, level_up | |
| def check_and_award_badges(data: dict, context: dict) -> list: | |
| """ | |
| Check badge conditions and award new ones. | |
| context keys: score, topics_count, quizzes_count, event | |
| Returns list of newly awarded badge keys. | |
| """ | |
| earned = set(data.get("badges", [])) | |
| new_ones = [] | |
| score = context.get("score", -1) | |
| topics_count = context.get("topics_count", 0) | |
| quizzes_count = context.get("quizzes_count", 0) | |
| event = context.get("event", "") | |
| streak = data.get("streak", 0) | |
| xp = data.get("xp", 0) | |
| checks = { | |
| "first_quiz": quizzes_count >= 1, | |
| "perfect_score": score == 100, | |
| "streak_3": streak >= 3, | |
| "streak_7": streak >= 7, | |
| "streak_14": streak >= 14, | |
| "topics_5": topics_count >= 5, | |
| "topics_10": topics_count >= 10, | |
| "quizzes_10": quizzes_count >= 10, | |
| "quizzes_25": quizzes_count >= 25, | |
| "score_above_80": score >= 80, | |
| "flashcards": event == "flashcards", | |
| "level_scholar": xp >= 300, | |
| "level_expert": xp >= 500, | |
| "level_master": xp >= 800, | |
| "notes_saver": event == "note_saved", | |
| } | |
| for key, condition in checks.items(): | |
| if condition and key not in earned: | |
| earned.add(key) | |
| new_ones.append(key) | |
| data["badges"] = list(earned) | |
| return new_ones | |
| def record_quiz(data: dict, score: int, topics_count: int) -> dict: | |
| data["total_quizzes"] = data.get("total_quizzes", 0) + 1 | |
| return data | |
| def get_xp_for_quiz(score: int) -> int: | |
| xp = XP_QUIZ_COMPLETE | |
| if score == 100: | |
| xp += XP_PERFECT_SCORE | |
| elif score >= 80: | |
| xp += XP_SCORE_ABOVE_80 | |
| elif score >= 60: | |
| xp += XP_SCORE_ABOVE_60 | |
| return xp | |