File size: 6,817 Bytes
f9766bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
"""
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