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