274 lines
11 KiB
Python
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,
|
|
} |