initial commit (backend)
This commit is contained in:
+330
@@ -0,0 +1,330 @@
|
||||
import json
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from auth import require_auth
|
||||
from database import get_pool
|
||||
|
||||
router = APIRouter(prefix="/game", tags=["game"])
|
||||
|
||||
|
||||
@router.get("/progress")
|
||||
async def get_progress(request: Request):
|
||||
telegram_id, _ = await require_auth(request)
|
||||
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
user = await conn.fetchrow(
|
||||
"SELECT last_completed_level, current_level FROM users WHERE telegram_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
if not user:
|
||||
return {
|
||||
"last_completed_level": 0,
|
||||
"total_score": 0,
|
||||
"current_level": "A1"
|
||||
}
|
||||
|
||||
current_level = user["current_level"] or "A1"
|
||||
|
||||
# Очки из филвордов
|
||||
wordsearch_score = await conn.fetchval(
|
||||
"SELECT COALESCE(SUM(score), 0) FROM user_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
# Очки из предложений
|
||||
sentence_score = await conn.fetchval(
|
||||
"SELECT COALESCE(SUM(score), 0) FROM sentence_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
# Очки из квиза
|
||||
wordquiz_score = await conn.fetchval(
|
||||
"SELECT COALESCE(total_score, 0) FROM word_quiz_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
# Очки из Bubble Words
|
||||
bubblewords_score = await conn.fetchval(
|
||||
"SELECT COALESCE(total_score, 0) FROM bubble_words_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
|
||||
total_score = (wordsearch_score or 0) + (sentence_score or 0) + (wordquiz_score or 0) + (bubblewords_score or 0)
|
||||
|
||||
return {
|
||||
"last_completed_level": user["last_completed_level"] or 0,
|
||||
"total_score": total_score,
|
||||
"current_level": current_level
|
||||
}
|
||||
|
||||
|
||||
@router.get("/level/{level_number}")
|
||||
async def get_level(request: Request, level_number: int):
|
||||
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, last_completed_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"
|
||||
last_completed = user["last_completed_level"] or 0
|
||||
|
||||
# Проверяем, что уровень доступен (не дальше следующего за пройденным)
|
||||
if level_number > 1 and level_number > last_completed + 1:
|
||||
raise HTTPException(403, "Level not available yet. Complete previous level first.")
|
||||
|
||||
# Получаем уровень по его номеру ВНУТРИ выбранного уровня
|
||||
level = await conn.fetchrow(
|
||||
"SELECT * FROM levels WHERE level = $1 AND level_number = $2", current_level, level_number
|
||||
)
|
||||
|
||||
if not level:
|
||||
raise HTTPException(404, "Level not found")
|
||||
|
||||
words = await conn.fetch(
|
||||
"SELECT * FROM level_words WHERE level_id = $1", level["id"]
|
||||
)
|
||||
|
||||
progress = await conn.fetchrow(
|
||||
"SELECT * FROM user_progress WHERE user_id = $1 AND level_id = $2",
|
||||
telegram_id, level["id"],
|
||||
)
|
||||
|
||||
word_list = []
|
||||
for w in words:
|
||||
translation = w.get(f"translation_{lang}", "") or w.get("translation_pl", "") or w.get("translation_en", "")
|
||||
word_list.append({
|
||||
"word_pl": w["word_pl"],
|
||||
"translation": translation,
|
||||
})
|
||||
|
||||
grid_letters = level["grid_letters"]
|
||||
if isinstance(grid_letters, str):
|
||||
grid_letters = json.loads(grid_letters)
|
||||
|
||||
word_paths = level["word_paths"]
|
||||
if isinstance(word_paths, str):
|
||||
word_paths = json.loads(word_paths)
|
||||
|
||||
return {
|
||||
"level": {
|
||||
"id": level["id"],
|
||||
"level_number": level["level_number"],
|
||||
"grid_size": level["grid_size"],
|
||||
"grid_letters": grid_letters,
|
||||
"word_paths": word_paths,
|
||||
"level": level["level"],
|
||||
},
|
||||
"words": word_list,
|
||||
"progress": {
|
||||
"completed": progress is not None,
|
||||
"hints_used": progress["hints_used"] if progress else 0,
|
||||
"score": progress["score"] if progress else 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@router.post("/check-word")
|
||||
async def check_word(request: Request):
|
||||
telegram_id, _ = await require_auth(request)
|
||||
body = await request.json()
|
||||
level_id = body.get("level_id")
|
||||
selected_cells = body.get("cells", [])
|
||||
|
||||
if not level_id or not selected_cells:
|
||||
raise HTTPException(400, "level_id and cells required")
|
||||
|
||||
pool = await get_pool()
|
||||
found_word = None
|
||||
perfect_count = body.get("perfect_streak", 0)
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
level = await conn.fetchrow("SELECT * FROM levels WHERE id = $1", level_id)
|
||||
if not level:
|
||||
raise HTTPException(404, "Level not found")
|
||||
|
||||
word_paths = level["word_paths"]
|
||||
if isinstance(word_paths, str):
|
||||
word_paths = json.loads(word_paths)
|
||||
|
||||
grid_letters = level["grid_letters"]
|
||||
if isinstance(grid_letters, str):
|
||||
grid_letters = json.loads(grid_letters)
|
||||
|
||||
selected_word = ''
|
||||
for cell in selected_cells:
|
||||
r, c = cell[0], cell[1]
|
||||
try:
|
||||
selected_word += grid_letters[r][c]
|
||||
except (IndexError, TypeError):
|
||||
continue
|
||||
selected_word = selected_word.lower()
|
||||
|
||||
for word in word_paths.keys():
|
||||
if word.lower() == selected_word:
|
||||
found_word = word
|
||||
break
|
||||
|
||||
# 🔧 ИСПРАВЛЕНО: вызов достижений ВНЕ блока async with pool.acquire()
|
||||
if found_word:
|
||||
from routers.achievements import check_and_award_achievements
|
||||
|
||||
new_achievements = []
|
||||
if perfect_count > 0:
|
||||
ach = await check_and_award_achievements(telegram_id, "perfect_words", perfect_count)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"found": True,
|
||||
"word": found_word,
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
|
||||
return {"status": "ok", "found": False}
|
||||
|
||||
|
||||
@router.post("/complete-level")
|
||||
async def complete_level(request: Request):
|
||||
"""Завершение уровня с проверкой на читерство и выдачей достижений"""
|
||||
telegram_id, _ = await require_auth(request)
|
||||
body = await request.json()
|
||||
level_id = body.get("level_id")
|
||||
hints_used = body.get("hints_used", 0)
|
||||
completion_time = body.get("completion_time")
|
||||
perfect_streak = body.get("perfect_streak", 0)
|
||||
|
||||
if not level_id:
|
||||
raise HTTPException(400, "level_id required")
|
||||
|
||||
hints_used = min(max(0, hints_used), 3)
|
||||
|
||||
# Защита от читерства: минимальное время 5 секунд
|
||||
if completion_time is not None and completion_time < 5:
|
||||
raise HTTPException(400, "Completion time too fast. Minimum 5 seconds required.")
|
||||
|
||||
pool = await get_pool()
|
||||
|
||||
# 🔧 ИСПРАВЛЕНО: все проверки и запись прогресса — внутри одного блока
|
||||
# все данные для достижений собираем здесь, а сами вызовы делаем снаружи
|
||||
already_completed = False
|
||||
existing_score = 0
|
||||
score = 0
|
||||
levels_completed = 0
|
||||
grand_total = 0
|
||||
|
||||
async with pool.acquire() as conn:
|
||||
user = await conn.fetchrow(
|
||||
"SELECT id, telegram_id, last_completed_level, current_level FROM users WHERE telegram_id = $1",
|
||||
telegram_id
|
||||
)
|
||||
if not user:
|
||||
raise HTTPException(401, "User not registered")
|
||||
|
||||
level = await conn.fetchrow(
|
||||
"SELECT id, level_number, grid_size, level FROM levels WHERE id = $1", level_id
|
||||
)
|
||||
if not level:
|
||||
raise HTTPException(404, "Level not found")
|
||||
|
||||
current_level_number = level["level_number"]
|
||||
level_name = level["level"] or "A1"
|
||||
|
||||
# Проверяем, что уровень принадлежит текущему уровню пользователя
|
||||
if level_name != (user["current_level"] or "A1"):
|
||||
raise HTTPException(403, "This level does not belong to your current level")
|
||||
|
||||
existing_progress = await conn.fetchrow(
|
||||
"SELECT score FROM user_progress WHERE user_id = $1 AND level_id = $2",
|
||||
telegram_id, level_id
|
||||
)
|
||||
|
||||
if existing_progress:
|
||||
already_completed = True
|
||||
existing_score = existing_progress["score"]
|
||||
else:
|
||||
score = max(10, 100 - hints_used * 10)
|
||||
|
||||
await conn.execute(
|
||||
"""INSERT INTO user_progress (user_id, level_id, completed_at, hints_used, score, level)
|
||||
VALUES ($1, $2, NOW(), $3, $4, $5)""",
|
||||
telegram_id, level_id, hints_used, score, level_name,
|
||||
)
|
||||
|
||||
# Обновляем last_completed_level (максимальный номер пройденного уровня)
|
||||
await conn.execute(
|
||||
"""UPDATE users
|
||||
SET last_completed_level = GREATEST(last_completed_level, $2)
|
||||
WHERE telegram_id = $1""",
|
||||
telegram_id, current_level_number,
|
||||
)
|
||||
|
||||
# Собираем данные для достижений внутри блока
|
||||
levels_completed = await conn.fetchval(
|
||||
"SELECT COUNT(*) FROM user_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
) or 0
|
||||
|
||||
total_score = await conn.fetchval(
|
||||
"SELECT COALESCE(SUM(score), 0) FROM user_progress WHERE user_id = $1",
|
||||
telegram_id
|
||||
) or 0
|
||||
sentence_score = await conn.fetchval(
|
||||
"SELECT COALESCE(SUM(score), 0) FROM sentence_progress WHERE user_id = $1",
|
||||
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 = total_score + sentence_score + wordquiz_score + bubblewords_score
|
||||
|
||||
# Если уровень уже был пройден — просто возвращаем результат
|
||||
if already_completed:
|
||||
return {
|
||||
"status": "ok",
|
||||
"score": existing_score,
|
||||
"already_completed": True,
|
||||
"new_achievements": []
|
||||
}
|
||||
|
||||
# 🔧 ИСПРАВЛЕНО: вызовы достижений ВНЕ блока async with pool.acquire()
|
||||
from routers.achievements import check_and_award_achievements
|
||||
new_achievements = []
|
||||
|
||||
ach = await check_and_award_achievements(telegram_id, "levels_completed", levels_completed)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
if hints_used == 0:
|
||||
ach = await check_and_award_achievements(telegram_id, "no_hints_level", 1)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
if completion_time is not None and completion_time > 0:
|
||||
if int(completion_time) <= 60:
|
||||
ach = await check_and_award_achievements(telegram_id, "speed_level", 60)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
ach = await check_and_award_achievements(telegram_id, "total_score", grand_total)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
if perfect_streak >= 5:
|
||||
ach = await check_and_award_achievements(telegram_id, "perfect_words", perfect_streak)
|
||||
new_achievements.extend(ach)
|
||||
|
||||
return {
|
||||
"status": "ok",
|
||||
"score": score,
|
||||
"already_completed": False,
|
||||
"new_achievements": new_achievements
|
||||
}
|
||||
Reference in New Issue
Block a user