Files
filwordspl-backend/routers/bubblewords.py
T
2026-09-20 11:45:58 +00:00

274 lines
11 KiB
Python

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,
}