import random import time import hashlib # 🔧 НОВОЕ: импорт hashlib для стабильного хэширования from fastapi import APIRouter, Request, HTTPException from auth import require_auth from database import get_pool router = APIRouter(prefix="/bubblewords", tags=["bubblewords"]) def split_word_into_parts(word: str, forbidden_parts: set = None) -> list: """Разбивает польское слово на части по 1-3 буквы. forbidden_parts — части, которые уже заняты другими словами на уровне. Если не удалось найти уникальное разбиение — добавляет суффикс с индексом слова.""" if forbidden_parts is None: forbidden_parts = set() n = len(word) attempts = 0 max_attempts = 50 # Увеличиваем попытки while attempts < max_attempts: parts = [] i = 0 while i < n: remaining = n - i if remaining == 1: parts.append(word[i:]) i += 1 elif remaining == 2: if random.random() < 0.4: parts.append(word[i:i+1]) parts.append(word[i+1:]) else: parts.append(word[i:]) i += 2 elif remaining == 3: choice = random.choice([1, 2, 3]) if choice == 1: parts.append(word[i:i+1]) parts.append(word[i+1:]) elif choice == 2: parts.append(word[i:i+2]) parts.append(word[i+2:]) else: parts.append(word[i:]) i += 3 elif remaining == 4: choice = random.choice([(2, 2), (1, 3), (3, 1), (1, 1, 2), (2, 1, 1)]) for size in choice: parts.append(word[i:i+size]) i += size elif remaining == 5: choice = random.choice([(2, 3), (3, 2), (2, 2, 1), (2, 1, 2)]) for size in choice: parts.append(word[i:i+size]) i += size elif remaining == 6: choice = random.choice([(3, 3), (2, 2, 2), (3, 2, 1), (2, 3, 1)]) for size in choice: parts.append(word[i:i+size]) i += size else: size = random.choice([2, 3]) if i + size > n: size = n - i parts.append(word[i:i+size]) i += size # Проверяем конфликт conflict = False for part in parts: if part in forbidden_parts: conflict = True break if not conflict: return parts attempts += 1 # Если за 50 попыток не нашли уникальное разбиение — # разбиваем слово ПО БУКВАМ и добавляем суффикс с хешем слова # Такой суффикс гарантированно уникален для каждого слова # 🔧 ИСПРАВЛЕНО: заменён hash() на hashlib.md5 для стабильности между запусками word_hash = hashlib.md5(word.encode('utf-8')).hexdigest()[:4] # Берём первые 4 символа MD5 unique_parts = [] for idx, letter in enumerate(word): unique_parts.append(f"{letter}.{word_hash}.{idx}") return unique_parts @router.get("/next") async def get_next_bubble_level(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 levels_completed FROM bubble_words_progress WHERE user_id = $1", telegram_id ) current_bubble_level = (progress["levels_completed"] if progress else 0) + 1 # Прогрессия по количеству слов на уровне if current_bubble_level <= 10: num_words = random.choice([3, 4]) elif current_bubble_level <= 25: num_words = random.choice([5, 6]) else: num_words = random.choice([7, 8]) # Запрос слов с фильтром по уровню и level_id IS NULL words = await conn.fetch(""" 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 $2 """, current_level, num_words) if len(words) < 2: raise HTTPException(404, "Not enough words") translation_field = f"translation_{lang}" result_words = [] all_parts = [] forbidden_parts = set() for w in words: word_pl = w["word_pl"] parts = split_word_into_parts(word_pl, forbidden_parts) translation = w.get(translation_field) or w.get("translation_en") or word_pl result_words.append({ "word_pl": word_pl, "translation": translation, "parts": parts, }) for part in parts: forbidden_parts.add(part) all_parts.append({ "text": part, "word_pl": word_pl, }) random.shuffle(all_parts) return { "words": result_words, "bubbles": all_parts, "level": current_bubble_level, } @router.post("/complete") async def complete_bubble_level(request: Request): """Завершение уровня пузырей, начисление очков и проверка достижений""" telegram_id, _ = await require_auth(request) body = await request.json() hints_used = body.get("hints_used", 0) words_count = body.get("words_count", 3) completion_time = body.get("completion_time") hints_used = min(max(0, hints_used), 3) score = max(10, 100 - hints_used * 10) pool = await get_pool() async with pool.acquire() as conn: user_exists = await conn.fetchval( "SELECT 1 FROM users WHERE telegram_id = $1", telegram_id ) if not user_exists: raise HTTPException(401, "User not registered") # Получаем current_level пользователя current_level = await conn.fetchval( "SELECT current_level FROM users WHERE telegram_id = $1", telegram_id ) or "A1" await conn.execute(""" INSERT INTO bubble_words_progress (user_id, levels_completed, total_score, level) VALUES ($1, 1, $2, $3) ON CONFLICT (user_id) DO UPDATE SET levels_completed = bubble_words_progress.levels_completed + 1, total_score = bubble_words_progress.total_score + $2, level = $3 """, telegram_id, score, current_level) # Проверка достижений from routers.achievements import check_and_award_achievements new_achievements = [] total_levels = await conn.fetchval( "SELECT levels_completed FROM bubble_words_progress WHERE user_id = $1", telegram_id ) or 0 ach = await check_and_award_achievements(telegram_id, "bubble_levels", total_levels) new_achievements.extend(ach) if hints_used == 0: ach = await check_and_award_achievements(telegram_id, "bubble_no_hints", 1) new_achievements.extend(ach) if score == 100: ach = await check_and_award_achievements(telegram_id, "bubble_perfect", 1) new_achievements.extend(ach) if completion_time is not None and completion_time > 0 and int(completion_time) <= 45: ach = await check_and_award_achievements(telegram_id, "bubble_speed", 45) 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 { "status": "ok", "score": score, "completion_time": completion_time, "new_achievements": new_achievements, } @router.get("/progress") async def get_bubble_progress(request: Request): """Статистика пользователя в Bubble Words""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: progress = await conn.fetchrow( "SELECT levels_completed, total_score FROM bubble_words_progress WHERE user_id = $1", telegram_id ) return { "levels_completed": progress["levels_completed"] if progress else 0, "total_score": progress["total_score"] if progress else 0, }