import csv import random from fastapi import APIRouter, Request, HTTPException from auth import require_auth from database import get_pool router = APIRouter(prefix="/sentences", tags=["sentences"]) @router.get("/{sentence_id}/hint") async def get_sentence_hint(sentence_id: int, request: Request): """Получить правильное предложение в качестве подсказки (одноразово)""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: row = await conn.fetchrow( "SELECT sentence_pl FROM sentences WHERE id = $1", sentence_id ) if not row: raise HTTPException(404, "Sentence not found") return { "sentence_pl": row["sentence_pl"], } def load_sentences_from_csv(filename): """Загружает предложения из CSV файла (формат: polish,english,russian,ukrainian).""" sentences = [] with open(filename, 'r', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: sentences.append({ "sentence_pl": row["polish"].strip(), "translation_ru": row.get("russian", ""), "translation_uk": row.get("ukrainian", ""), "translation_en": row.get("english", ""), }) return sentences async def init_sentences(): """Инициализирует таблицу предложений из CSV файлов по уровням.""" pool = await get_pool() async with pool.acquire() as conn: level_files = [ ("A1", "sentences_a1.csv"), ("A2", "sentences_a2.csv"), ("B1", "sentences_b1.csv"), ] for level, filename in level_files: sentences = load_sentences_from_csv(filename) for i, s in enumerate(sentences): words_count = len(s["sentence_pl"].split()) difficulty = 1 if words_count <= 4 else 2 if words_count <= 6 else 3 await conn.execute( """INSERT INTO sentences (sentence_number, sentence_pl, translation_ru, translation_uk, translation_en, difficulty, level) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (level, sentence_number) DO NOTHING""", i + 1, s["sentence_pl"], s["translation_ru"], s["translation_uk"], s["translation_en"], difficulty, level ) def normalize_word_for_display(word: str) -> str: """Убирает знаки конца предложения и приводит к нижнему регистру. Запятые, точки с запятой, двоеточия и кавычки ОСТАЮТСЯ — игрок должен их видеть.""" return word.strip('.!?…').lower() def normalize_word_for_check(word: str) -> str: """Убирает ВСЮ пунктуацию для сравнения — игрок не обязан расставлять запятые.""" return word.strip('.,!?;:()[]{}"\'-…').lower() @router.get("/next") async def get_next_sentence(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" # Находим случайное непройденное предложение нужного уровня row = await conn.fetchrow(""" SELECT s.* FROM sentences s WHERE s.level = $1 AND s.id NOT IN ( SELECT sentence_id FROM sentence_progress WHERE user_id = $2 ) ORDER BY RANDOM() LIMIT 1 """, current_level, telegram_id) if not row: return {"completed": True, "message": "All sentences completed!"} # Оригинальные слова (для проверки) correct_words = row["sentence_pl"].split() # Слова для ОТОБРАЖЕНИЯ — запятые остаются, убираем только точки/воскл/вопрос display_words = [normalize_word_for_display(w) for w in correct_words] shuffled_words = display_words.copy() random.shuffle(shuffled_words) translation_field = f"translation_{lang}" translation = row.get(translation_field, "") or row.get("translation_en", "") return { "id": row["id"], "sentence_number": row["sentence_number"], "words": shuffled_words, "correct_words": display_words, "translation": translation, "difficulty": row["difficulty"], "level": row["level"], } @router.post("/check") async def check_sentence(request: Request): """Проверка предложения с защитой от повторного прохождения и выдачей достижений""" telegram_id, _ = await require_auth(request) body = await request.json() sentence_id = body.get("sentence_id") user_words = body.get("words", []) completion_time = body.get("completion_time") if not sentence_id or not user_words: raise HTTPException(400, "sentence_id and words required") if len(user_words) > 20: raise HTTPException(400, "Too many words") 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") row = await conn.fetchrow( "SELECT id, sentence_pl, level FROM sentences WHERE id = $1", sentence_id ) if not row: raise HTTPException(404, "Sentence not found") existing_progress = await conn.fetchrow( "SELECT score, completed_at FROM sentence_progress WHERE user_id = $1 AND sentence_id = $2", telegram_id, sentence_id ) if existing_progress: return { "correct": True, "correct_words": [normalize_word_for_display(w) for w in row["sentence_pl"].split()], "already_completed": True, "score": existing_progress["score"], "completed_at": existing_progress["completed_at"].isoformat() if existing_progress["completed_at"] else None, "new_achievements": [] } # Защита от читерства: минимальное время 3 секунды if completion_time is not None and completion_time < 3: return { "correct": False, "correct_words": [normalize_word_for_display(w) for w in row["sentence_pl"].split()], "already_completed": False, "new_achievements": [], "error": "Too fast. Please take your time." } # Нормализуем для СРАВНЕНИЯ — без запятых и прочей пунктуации correct_words_original = row["sentence_pl"].split() correct_words_normalized = [normalize_word_for_check(w) for w in correct_words_original] user_words_normalized = [normalize_word_for_check(w) for w in user_words] is_correct = user_words_normalized == correct_words_normalized if is_correct: word_count = len(correct_words_original) if word_count <= 4: score = 50 elif word_count <= 6: score = 75 else: score = 100 await conn.execute( """INSERT INTO sentence_progress (user_id, sentence_id, completed_at, score, level) VALUES ($1, $2, NOW(), $3, $4)""", telegram_id, sentence_id, score, row["level"], ) from routers.achievements import check_and_award_achievements new_achievements = [] sentences_completed = await conn.fetchval( "SELECT COUNT(*) FROM sentence_progress WHERE user_id = $1", telegram_id ) ach = await check_and_award_achievements(telegram_id, "sentences_completed", sentences_completed) new_achievements.extend(ach) if completion_time is not None and completion_time > 0: ach = await check_and_award_achievements(telegram_id, "speed_sentence", int(completion_time)) 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, "correct_words": [normalize_word_for_display(w) for w in correct_words_original], "already_completed": False, "score": score, "new_achievements": new_achievements } return { "correct": False, "correct_words": [normalize_word_for_display(w) for w in correct_words_original], "already_completed": False, "new_achievements": [] } @router.get("/stats") async def get_sentence_stats(request: Request): """Статистика по предложениям для выбранного уровня""" telegram_id, _ = await require_auth(request) pool = await get_pool() async with pool.acquire() as conn: user = await conn.fetchrow( "SELECT current_level FROM users WHERE telegram_id = $1", telegram_id ) current_level = user["current_level"] if user else "A1" total = await conn.fetchval( "SELECT COUNT(*) FROM sentences WHERE level = $1", current_level ) completed = await conn.fetchval( "SELECT COUNT(*) FROM sentence_progress WHERE user_id = $1 AND level = $2", telegram_id, current_level ) total_score = await conn.fetchval( "SELECT COALESCE(SUM(score), 0) FROM sentence_progress WHERE user_id = $1 AND level = $2", telegram_id, current_level ) return { "total_sentences": total, "completed": completed or 0, "remaining": total - (completed or 0), "total_score": total_score or 0, "progress_percent": round(((completed or 0) / total * 100), 1) if total > 0 else 0, "level": current_level, }