Code : back/app/services/progress.py (CHALLENGES, bump_challenges, _advance), back/app/services/badges.py (CATALOG, check_after_answer, check_days_played, check_after_duel, close_season), back/app/services/matches.py (reward_duel), back/app/routers/creation.py, back/app/routers/me.py.
Pas de tirage : les trois mêmes défis chaque jour (progress.CHALLENGES, recopiés dans la table challenges par
ensure_challenges au seed).
| Id | Cible | Avance quand | Comment |
|---|---|---|---|
c_questions |
10 | chaque register_answer (fil et parties entre potes ; pas une question déjà jouée, qui n'est pas enregistrée) |
+1 |
c_combo |
5 | chaque register_answer |
progrès = max(progrès, users.combo) |
c_create |
1 | POST /questions accepté (après le test de doublon, avant la modération) |
+1 |
Table user_challenges (user, défi, jour local) : progress, rewarded.
_advance(uc, value, inc):
progrès = min(cible, value is None ? progrès + inc : max(progrès, value))
si progrès ≥ cible et pas rewarded :
rewarded = true
award_xp(XP_CHALLENGE = 100, "challenge", play = true)
clock.player_today(user) (minuit dans users.timezone) ; un nouveau jour = une nouvelle ligne.bump_challenges et par GET /me/challenges / GET /me/progress.challenges_summary compte les défis bouclés du jour sans rien créer (challenges_done de POST /answers).XP_AD_CHALLENGES (300), une fois par jour (ads.grant, flag ADS_ENABLED).badges.CATALOG (code, famille, nom, description, secret), recopié en table badges à chaque démarrage (ensure_badges : ajoute et corrige noms et descriptions).award(user, code, season, category). Clé d'unicité (badge, saison, thème) : un badge sans saison ni thème ne se gagne qu'une fois ; la maîtrise une fois par thème ; les badges de fin de mois une fois par saison et par thème.check_after_answer lit une seule fois les badges possédés (owned_badges) puis teste tout.rarity_map = round(100 × joueurs distincts ayant le badge / nombre de lignes users, 1), recalculée à chaque sérialisation.| Famille | Déclencheur | Fonction |
|---|---|---|
| Performance, secret Noctambule, maîtrise | chaque POST /answers (pas les parties entre potes) |
check_after_answer |
| Assiduité (jours, fondateur) | POST /answers, GET /me, GET /me/badges |
check_days_played |
| Duel, secret Remontada | fin d'une partie à deux joueurs qui ont joués, sans égalité, pour le vainqueur | matches.reward_duel → check_after_duel |
| Fin de mois | clôture de la saison (rollup) | close_season |
| Code | Condition exacte |
|---|---|
perf_reflex |
bonne réponse et 0 < elapsed_ms < 1000 |
perf_flawless_50 |
users.combo ≥ 50 (vérifié même sur une mauvaise réponse, où le combo vient de tomber à 0) |
perf_chrono_master |
format rapid et rapid_score > 20 |
secret_night_owl |
bonne réponse, heure locale (users.timezone) ∈ [3 h, 5 h[ |
mastery_bronze |
thème de la question : correct ≥ 50 et round(100 × correct / answers) ≥ 60 |
mastery_silver |
≥ 200 et ≥ 70 % |
mastery_gold |
≥ 500 et ≥ 80 % |
mastery_platinum |
≥ 1 000 et ≥ 85 % |
mastery_diamond |
≥ 2 500 et ≥ 90 % |
days_7 / 30 / 100 / 365 |
users.days_played ≥ N (jours locaux distincts avec une réponse) |
founder |
users.is_founder |
duel_10 / 100 / 1000 |
victoires en duel (toutes, partie en cours comprise) ≥ N |
duel_unbeaten_20 |
victoires d'affilée depuis la plus récente ≥ 20 (une défaite ou une égalité casse la série) |
duel_giant_killer |
rang du perdant dans lb:{saison courante}:all:world < 100 au moment de la fin |
duel_remontada |
manche 0 : vainqueur faux et perdant juste |
season_top1 / 10 / 50 / 100 |
rang 1, ≤ 10, ≤ 50, ≤ 100 dans lb:{s}:{thème}:world, pour all et chaque thème |
season_fr_top50 |
rang ≤ 50 dans lb:{s}:all:FR |
season_following_top1 |
meilleur score all parmi (suivis ∪ soi), au moins un suivi, score > 0 |
Maîtrise : seulement le thème de la question répondue ; plusieurs paliers peuvent tomber d'un coup. Les rejeux
ne comptent pas dans user_category_stats (une seule réponse par joueur et par question, voir
Progression). Détail de la clôture : Classements.
Duel = partie à exactement 2 joueurs ayant tous deux joué (duels.duels_query). Les badges de duel ne sont pas
renvoyés à l'app sur le moment (pas de badges_unlocked) ; ils apparaissent dans le profil.
rapid_score > 20 ; les Chrono 30 s du seed ont 10 items, et le format rapid n'est ni créable ni importable (CREATABLE_KINDS).check_after_answer n'est appelé que par POST /answers. Jours joués et fondateur : aussi à GET /me.users.combo n'est pas remis à zéro à minuit : 4 bonnes hier + 1 aujourd'hui bouclent le défi.is_founder n'est mis à vrai que par le seed de démo.users, comptes supprimés et compte officiel compris.