"""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"), ], }, ] # 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 FROM (SELECT 1) """, (user_id, user_id, user_id, user_id)).fetchone() streak_row = conn.execute( "SELECT current_streak FROM users WHERE id=?", (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), } 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, 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)).fetchone() streak_row = conn.execute( "SELECT current_streak, max_streak FROM users WHERE id=?", (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), } # 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, }