"""Ban Yaro — Gamification: Badges, Streaks, Punkte""" from datetime import date, timedelta from fastapi import APIRouter, Depends from database import db from auth import get_current_user router = APIRouter() # ------------------------------------------------------------------ # Tier-Definitionen # ------------------------------------------------------------------ TIERS = { "bronze": {"name": "Bronze", "color": "#cd7f32", "dark": "#7c4a0a", "text": "#fff"}, "silber": {"name": "Silber", "color": "#94a3b8", "dark": "#475569", "text": "#fff"}, "gold": {"name": "Gold", "color": "#f59e0b", "dark": "#b45309", "text": "#fff"}, "platin": {"name": "Platin", "color": "#cbd5e1", "dark": "#94a3b8", "text": "#1e293b"}, "diamant": {"name": "Diamant", "color": "#67e8f9", "dark": "#0891b2", "text": "#fff"}, } TIER_ORDER = ["bronze", "silber", "gold", "platin", "diamant"] # ------------------------------------------------------------------ # Badge-Kategorien mit Stufen # ------------------------------------------------------------------ CATEGORIES = [ { "id": "km", "name": "Kilometer", "emoji": "🐾", "metrik": "total_km", "einheit": "km", "stufen": [ ("bronze", 5, "Erste Schritte"), ("silber", 25, "Vielläufer"), ("gold", 100, "100-km-Club"), ("platin", 500, "Ausdauer-Profi"), ("diamant", 2000, "Unermüdlich"), ], }, { "id": "routen", "name": "Routen", "emoji": "🗺️", "metrik": "routen", "einheit": "", "stufen": [ ("bronze", 1, "Entdecker"), ("silber", 5, "Kartograph"), ("gold", 20, "Routen-Profi"), ("platin", 50, "Gassi-Legende"), ("diamant", 100, "Route-König"), ], }, { "id": "pois", "name": "POIs", "emoji": "📍", "metrik": "pois", "einheit": "", "stufen": [ ("bronze", 1, "Pfadfinder"), ("silber", 5, "Community-Held"), ("gold", 20, "Botschafter"), ("platin", 50, "Karten-Profi"), ("diamant", 100, "Legende"), ], }, { "id": "streak", "name": "Streak", "emoji": "🔥", "metrik": "streak", "einheit": " Tage", "stufen": [ ("bronze", 3, "Auf Trab"), ("silber", 7, "Wochenheld"), ("gold", 30, "Monatsläufer"), ("platin", 100, "Eiserne Pfote"), ("diamant", 365, "Ein ganzes Jahr"), ], }, { "id": "wiki_fotos", "name": "Wiki-Fotos", "emoji": "📸", "metrik": "wiki_fotos", "einheit": " Foto(s)", "stufen": [ ("bronze", 1, "Erster Klick"), ("silber", 3, "Foto-Fan"), ("gold", 10, "Wiki-Fotograf"), ], }, { "id": "wetter_tapfer", "name": "Wetter-Tapferkeit", "emoji": "⛈️", "metrik": "wetter_tapfer_score", "einheit": " Eintrag/Einträge", "stufen": [ ("bronze", 1, "Regentrotzdem"), ("silber", 5, "Wettertrotzer"), ("gold", 15, "Allwetter-Held"), ("platin", 30, "Hunde-Wetterheld"), ], }, { "id": "jahreszeiten", "name": "Jahreszeiten-Erkunder", "emoji": "🍃", "metrik": "jahreszeiten_score", "einheit": " Jahreszeit(en)", "stufen": [ ("bronze", 1, "Frühlings-Erkunder"), ("silber", 2, "Sommer-Genießer"), ("gold", 3, "Herbst-Schnüffler"), ("platin", 4, "Alle-Jahreszeiten"), ], }, { "id": "schnee_held", "name": "Schneeheld", "emoji": "❄️", "metrik": "schnee_eintraege", "einheit": " Eintrag/Einträge", "stufen": [ ("bronze", 1, "Erster Schnee"), ("silber", 5, "Schneehund"), ("gold", 15, "Schneeheld"), ("platin", 30, "Schneewolf"), ], }, ] # Flat-Liste aller Badge-IDs für DB-Kompatibilität def _all_badge_ids(): ids = [] for cat in CATEGORIES: for tier, _, _ in cat["stufen"]: ids.append(f"{cat['id']}_{tier}") return ids # ------------------------------------------------------------------ # Streak aktualisieren # ------------------------------------------------------------------ def update_streak(user_id: int, conn): today = date.today().isoformat() row = conn.execute( "SELECT current_streak, max_streak, last_activity_date FROM users WHERE id=?", (user_id,) ).fetchone() if not row: return last = row["last_activity_date"] cur = row["current_streak"] or 0 mx = row["max_streak"] or 0 if last == today: return yesterday = (date.today() - timedelta(days=1)).isoformat() cur = cur + 1 if last == yesterday else 1 mx = max(mx, cur) conn.execute( "UPDATE users SET current_streak=?, max_streak=?, last_activity_date=? WHERE id=?", (cur, mx, today, user_id) ) # ------------------------------------------------------------------ # Badges prüfen und vergeben # ------------------------------------------------------------------ def check_and_award(user_id: int, conn): stats = conn.execute(""" SELECT ROUND( COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1) AS total_km, (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1) AS routen, (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?) AS pois, (SELECT COUNT(*) FROM wiki_foto_submissions WHERE user_id=? AND status='approved') AS wiki_fotos FROM (SELECT 1) """, (user_id, user_id, user_id, user_id, user_id)).fetchone() streak_row = conn.execute( "SELECT current_streak FROM users WHERE id=?", (user_id,) ).fetchone() # Wetter-Tapferkeit: Diary-Einträge bei schlechtem Wetter wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 OR CAST(json_extract(d.weather_json, '$.temp_c') AS REAL) < 2 OR CAST(json_extract(d.weather_json, '$.wind_kmh') AS REAL) > 50 ) """, (user_id,)).fetchone() # Jahreszeiten: Anzahl Jahreszeiten mit mind. 5 Diary-Einträgen jahreszeiten_row = conn.execute(""" SELECT (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() # Schnee: Diary-Einträge bei Schnee (weathercode 71-77) schnee_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary WHERE user_id = ? AND weather_json IS NOT NULL AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (user_id,)).fetchone() metrics = { "total_km": stats["total_km"] if stats else 0, "routen": stats["routen"] if stats else 0, "pois": stats["pois"] if stats else 0, "streak": (streak_row["current_streak"] if streak_row else 0), "wiki_fotos": stats["wiki_fotos"] if stats else 0, "wetter_tapfer_score": wetter_row["cnt"] if wetter_row else 0, "jahreszeiten_score": (jahreszeiten_row["jahreszeiten_score"] if jahreszeiten_row else 0), "schnee_eintraege": schnee_row["cnt"] if schnee_row else 0, } earned = {r["badge_id"] for r in conn.execute("SELECT badge_id FROM user_badges WHERE user_id=?", (user_id,)).fetchall()} new_badges = [] for cat in CATEGORIES: val = metrics.get(cat["metrik"], 0) for tier, schwelle, badge_name in cat["stufen"]: bid = f"{cat['id']}_{tier}" if bid not in earned and val >= schwelle: conn.execute( "INSERT OR IGNORE INTO user_badges (user_id, badge_id) VALUES (?,?)", (user_id, bid) ) new_badges.append({ "id": bid, "name": badge_name, "emoji": cat["emoji"], "tier": TIERS[tier]["name"], }) return new_badges # ------------------------------------------------------------------ # API # ------------------------------------------------------------------ @router.get("/me") async def my_achievements(user=Depends(get_current_user)): uid = user["id"] with db() as conn: update_streak(uid, conn) new_badges = check_and_award(uid, conn) stats = conn.execute(""" SELECT ROUND( COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1) AS total_km, (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1) AS routen, (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?) AS pois, (SELECT COUNT(*) FROM wiki_foto_submissions WHERE user_id=? AND status='approved') AS wiki_fotos, ROUND( COALESCE((SELECT SUM(r.distanz_km) FROM routes r WHERE r.user_id=? AND r.is_valid=1), 0) + COALESCE((SELECT SUM(w.walked_km) FROM route_walks w WHERE w.user_id=?), 0), 1)*1 + (SELECT COUNT(*) FROM user_map_pois p WHERE p.user_id=?)*5 + (SELECT COUNT(*) FROM routes r WHERE r.user_id=? AND r.is_valid=1)*10 AS punkte FROM (SELECT 1) """, (uid, uid, uid, uid, uid, uid, uid, uid, uid)).fetchone() streak_row = conn.execute( "SELECT current_streak, max_streak FROM users WHERE id=?", (uid,) ).fetchone() # Wetter-Tapferkeit wetter_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary d LEFT JOIN diary_dogs dd ON dd.diary_id = d.id WHERE d.user_id = ? AND d.weather_json IS NOT NULL AND ( CAST(json_extract(d.weather_json, '$.precip_prob') AS INTEGER) > 60 OR CAST(json_extract(d.weather_json, '$.temp_c') AS REAL) < 2 OR CAST(json_extract(d.weather_json, '$.wind_kmh') AS REAL) > 50 ) """, (uid,)).fetchone() # Jahreszeiten jahreszeiten_row = conn.execute(""" SELECT (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (3,4,5)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (6,7,8)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (9,10,11)) >= 5 THEN 1 ELSE 0 END) + (CASE WHEN (SELECT COUNT(*) FROM diary WHERE user_id=? AND CAST(strftime('%m', datum) AS INTEGER) IN (12,1,2)) >= 5 THEN 1 ELSE 0 END) AS jahreszeiten_score FROM (SELECT 1) """, (uid, uid, uid, uid)).fetchone() # Schnee-Einträge schnee_row = conn.execute(""" SELECT COUNT(*) AS cnt FROM diary WHERE user_id = ? AND weather_json IS NOT NULL AND CAST(json_extract(weather_json, '$.weathercode') AS INTEGER) BETWEEN 71 AND 77 """, (uid,)).fetchone() earned_rows = conn.execute( "SELECT badge_id FROM user_badges WHERE user_id=?", (uid,) ).fetchall() earned_ids = {r["badge_id"] for r in earned_rows} rank_row = conn.execute(""" SELECT COUNT(*)+1 AS rang FROM ( SELECT u.id, ROUND(COALESCE(SUM(r.distanz_km),0),1)*1 +COUNT(DISTINCT p.id)*5 +COUNT(DISTINCT r.id)*10 AS punkte FROM users u LEFT JOIN routes r ON r.user_id=u.id AND r.is_valid=1 LEFT JOIN user_map_pois p ON p.user_id=u.id GROUP BY u.id ) WHERE punkte > ? """, (stats["punkte"] if stats else 0,)).fetchone() metrics = { "total_km": stats["total_km"] if stats else 0, "routen": stats["routen"] if stats else 0, "pois": stats["pois"] if stats else 0, "streak": (streak_row["current_streak"] if streak_row else 0), "wiki_fotos": stats["wiki_fotos"] if stats else 0, "wetter_tapfer_score": wetter_row["cnt"] if wetter_row else 0, "jahreszeiten_score": (jahreszeiten_row["jahreszeiten_score"] if jahreszeiten_row else 0), "schnee_eintraege": schnee_row["cnt"] if schnee_row else 0, } # Kategorien mit aktuellem Tier + Fortschritt aufbauen badge_categories = [] for cat in CATEGORIES: val = metrics.get(cat["metrik"], 0) current_tier = None next_tier = None next_thresh = None prev_thresh = 0 for tier, schwelle, badge_name in cat["stufen"]: bid = f"{cat['id']}_{tier}" if val >= schwelle: current_tier = {"tier": tier, "badge_id": bid, "name": badge_name, **TIERS[tier]} prev_thresh = schwelle else: if next_tier is None: next_tier = {"tier": tier, "badge_id": bid, "name": badge_name, "schwelle": schwelle, **TIERS[tier]} next_thresh = schwelle break # Fortschritt zur nächsten Stufe in % if next_thresh: progress = round(min((val - prev_thresh) / (next_thresh - prev_thresh) * 100, 99)) else: progress = 100 badge_categories.append({ "id": cat["id"], "name": cat["name"], "emoji": cat["emoji"], "einheit": cat["einheit"], "current_value": val, "current_tier": current_tier, "next_tier": next_tier, "progress": progress, "alle_stufen": [ { "tier": tier, "schwelle": schwelle, "name": badge_name, "earned": f"{cat['id']}_{tier}" in earned_ids, **TIERS[tier], } for tier, schwelle, badge_name in cat["stufen"] ], }) return { "stats": dict(stats) if stats else {}, "streak": {"current": streak_row["current_streak"] if streak_row else 0, "max": streak_row["max_streak"] if streak_row else 0}, "rang": rank_row["rang"] if rank_row else 1, "categories": badge_categories, "new_badges": new_badges, }