import random from fastapi import APIRouter, Request, HTTPException from auth import require_auth from database import get_pool router = APIRouter(prefix="/wordquiz", tags=["wordquiz"]) @router.get("/next") async def get_next_quiz(request: Request): """Выдаёт случайное слово из базы выбранного уровня. Исключает недавно показанные.""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: user = await conn.fetchrow( "SELECT app_language, current_level FROM users WHERE telegram_id = $1", telegram_id ) if not user: raise HTTPException(401, "User not registered") lang = user["app_language"] or "pl" current_level = user["current_level"] or "A1" progress = await conn.fetchrow( "SELECT words_guessed, recent_words FROM word_quiz_progress WHERE user_id = $1", telegram_id ) total_guessed = progress["words_guessed"] if progress else 0 recent_words = list(progress["recent_words"]) if progress and progress["recent_words"] else [] async def pick_word(exclude, respect_exclude=True): if respect_exclude and exclude: placeholders = ",".join([f"${i+2}" for i in range(len(exclude))]) return await conn.fetchrow(f""" SELECT word_pl, translation_ru, translation_uk, translation_en FROM ( SELECT DISTINCT ON (word_pl) word_pl, translation_ru, translation_uk, translation_en FROM level_words WHERE level = $1 AND level_id IS NULL AND word_pl NOT IN ({placeholders}) ) AS unique_words ORDER BY RANDOM() LIMIT 1 """, current_level, *exclude) return await conn.fetchrow(""" SELECT word_pl, translation_ru, translation_uk, translation_en FROM ( SELECT DISTINCT ON (word_pl) word_pl, translation_ru, translation_uk, translation_en FROM level_words WHERE level = $1 AND level_id IS NULL ) AS unique_words ORDER BY RANDOM() LIMIT 1 """, current_level) word = await pick_word(recent_words, respect_exclude=True) if not word: word = await pick_word([], respect_exclude=False) if not word: raise HTTPException(404, "No words found") correct_polish = word["word_pl"] translation_field = f"translation_{lang}" translation = word.get(translation_field) or word.get("translation_en") or correct_polish # Ищем неправильные варианты exclude_for_wrong = recent_words + [correct_polish] placeholders = ",".join([f"${i+3}" for i in range(len(exclude_for_wrong))]) wrong_words = await conn.fetch(f""" SELECT word_pl FROM ( SELECT DISTINCT ON (word_pl) word_pl FROM level_words WHERE level = $1 AND level_id IS NULL AND word_pl != $2 AND word_pl NOT IN ({placeholders}) ) AS unique_wrong ORDER BY RANDOM() LIMIT 5 """, current_level, correct_polish, *exclude_for_wrong) if len(wrong_words) < 2: wrong_words = await conn.fetch(""" SELECT word_pl FROM ( SELECT DISTINCT ON (word_pl) word_pl FROM level_words WHERE level = $1 AND level_id IS NULL AND word_pl != $2 ) AS unique_wrong ORDER BY RANDOM() LIMIT 5 """, current_level, correct_polish) wrong_options = [] seen = {correct_polish.lower()} for w in wrong_words: wl = w["word_pl"].lower() if wl not in seen: wrong_options.append(w["word_pl"]) seen.add(wl) if len(wrong_options) >= 2: break recent_words = recent_words + [correct_polish] + wrong_options if len(recent_words) > 45: recent_words = recent_words[-45:] await conn.execute(""" INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, recent_words) VALUES ($1, 0, 0, $2::text[]) ON CONFLICT (user_id) DO UPDATE SET recent_words = $2::text[] """, telegram_id, recent_words) options = wrong_options + [correct_polish] random.shuffle(options) if total_guessed <= 10: difficulty = "easy" elif total_guessed <= 30: difficulty = "medium" elif total_guessed <= 60: difficulty = "hard" else: difficulty = "expert" return { "correct_polish": correct_polish, "translation": translation, "options": options, "difficulty": difficulty, } @router.post("/check") async def check_quiz_answer(request: Request): """Проверяет ответ, начисляет очки и проверяет достижения""" telegram_id, _ = await require_auth(request) body = await request.json() correct_polish = body.get("correct_polish", "") user_answer = body.get("answer", "") mistakes = body.get("mistakes", 0) completion_time = body.get("completion_time") if not correct_polish or not user_answer: raise HTTPException(400, "correct_polish and answer required") # Защита от читерства: минимальное время 1 секунда if completion_time is not None and completion_time < 1: return { "correct": False, "correct_streak": 0, "error": "Too fast. Please take your time." } is_correct = user_answer.strip().lower() == correct_polish.strip().lower() pool = await get_pool() async with pool.acquire() as conn: current_streak = await conn.fetchval( "SELECT correct_streak FROM word_quiz_progress WHERE user_id = $1", telegram_id ) or 0 if is_correct: current_streak += 1 else: current_streak = 0 if is_correct: score = max(10, 50 - (mistakes * 20)) await conn.execute(""" INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak) VALUES ($1, 1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET words_guessed = word_quiz_progress.words_guessed + 1, total_score = word_quiz_progress.total_score + $2, correct_streak = $3 """, telegram_id, score, current_streak) from routers.achievements import check_and_award_achievements new_achievements = [] total_guessed = await conn.fetchval( "SELECT words_guessed FROM word_quiz_progress WHERE user_id = $1", telegram_id ) or 0 ach = await check_and_award_achievements(telegram_id, "quiz_guessed", total_guessed) new_achievements.extend(ach) if current_streak >= 10: ach = await check_and_award_achievements(telegram_id, "quiz_perfect_streak", current_streak) new_achievements.extend(ach) if mistakes == 0: ach = await check_and_award_achievements(telegram_id, "quiz_no_mistakes", 1) new_achievements.extend(ach) if completion_time is not None and completion_time > 0 and int(completion_time) <= 3: ach = await check_and_award_achievements(telegram_id, "quiz_speed", 3) new_achievements.extend(ach) wordsearch_score = await conn.fetchval( "SELECT COALESCE(SUM(score), 0) FROM user_progress WHERE user_id = $1 AND completed_at IS NOT NULL", telegram_id ) or 0 sentence_score = await conn.fetchval( "SELECT COALESCE(SUM(score), 0) FROM sentence_progress WHERE user_id = $1 AND completed_at IS NOT NULL", telegram_id ) or 0 wordquiz_score = await conn.fetchval( "SELECT COALESCE(total_score, 0) FROM word_quiz_progress WHERE user_id = $1", telegram_id ) or 0 bubblewords_score = await conn.fetchval( "SELECT COALESCE(total_score, 0) FROM bubble_words_progress WHERE user_id = $1", telegram_id ) or 0 grand_total = wordsearch_score + sentence_score + wordquiz_score + bubblewords_score ach = await check_and_award_achievements(telegram_id, "total_score", grand_total) new_achievements.extend(ach) return { "correct": True, "score": score, "correct_streak": current_streak, "new_achievements": new_achievements, } else: # При 3 ошибках — начисляем утешительные 10 очков if mistakes >= 3: await conn.execute(""" INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak) VALUES ($1, 0, 10, 0) ON CONFLICT (user_id) DO UPDATE SET total_score = word_quiz_progress.total_score + 10, correct_streak = 0 """, telegram_id) else: await conn.execute(""" INSERT INTO word_quiz_progress (user_id, words_guessed, total_score, correct_streak) VALUES ($1, 0, 0, $2) ON CONFLICT (user_id) DO UPDATE SET correct_streak = $2 """, telegram_id, current_streak) return { "correct": False, "correct_streak": current_streak, "consolation_score": 10 if mistakes >= 3 else 0, } @router.get("/progress") async def get_quiz_progress(request: Request): """Статистика пользователя в квизе""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: progress = await conn.fetchrow( "SELECT words_guessed, total_score, correct_streak FROM word_quiz_progress WHERE user_id = $1", telegram_id ) total_guessed = progress["words_guessed"] if progress else 0 if total_guessed <= 10: difficulty = "easy" elif total_guessed <= 30: difficulty = "medium" elif total_guessed <= 60: difficulty = "hard" else: difficulty = "expert" return { "words_guessed": total_guessed, "total_score": progress["total_score"] if progress else 0, "correct_streak": progress["correct_streak"] if progress else 0, "difficulty": difficulty }