initial commit (backend)
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user