904 lines
34 KiB
Python
904 lines
34 KiB
Python
|
|
"""Обработчики мини-игр"""
|
|||
|
|
import asyncio
|
|||
|
|
import random
|
|||
|
|
from datetime import datetime
|
|||
|
|
from typing import Optional
|
|||
|
|
|
|||
|
|
from aiogram import Router, F
|
|||
|
|
from aiogram.filters import Command
|
|||
|
|
from aiogram.types import Message, CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
|||
|
|
from aiogram.fsm.context import FSMContext
|
|||
|
|
from aiogram.fsm.state import State, StatesGroup
|
|||
|
|
|
|||
|
|
from database.db import async_session_maker
|
|||
|
|
from services.user_service import UserService
|
|||
|
|
from services.vocabulary_service import VocabularyService
|
|||
|
|
from services.ai_service import AIService
|
|||
|
|
from utils.i18n import t, get_user_lang, get_user_translation_lang
|
|||
|
|
|
|||
|
|
router = Router()
|
|||
|
|
ai_service = AIService()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class SpeedRoundStates(StatesGroup):
|
|||
|
|
"""Состояния для игры Speed Round"""
|
|||
|
|
playing = State()
|
|||
|
|
waiting_answer = State()
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MatchGameStates(StatesGroup):
|
|||
|
|
"""Состояния для игры Match (Найди пару)"""
|
|||
|
|
waiting_answer = State()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# Константы игры Speed Round
|
|||
|
|
SPEED_ROUND_COUNT = 10 # Количество слов в раунде
|
|||
|
|
SPEED_ROUND_TIME = 10 # Секунд на ответ
|
|||
|
|
POINTS_CORRECT = 100 # Базовые очки за правильный ответ
|
|||
|
|
POINTS_SPEED_BONUS = 10 # Бонус за каждую оставшуюся секунду
|
|||
|
|
|
|||
|
|
# Константы игры Match
|
|||
|
|
MATCH_WORDS_COUNT = 5 # Количество пар слов
|
|||
|
|
MATCH_TIME_LIMIT = 60 # Секунд на всю игру (0 = без таймера)
|
|||
|
|
MATCH_POINTS_PER_PAIR = 50 # Очки за правильную пару
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_minigames_menu_keyboard(lang: str) -> InlineKeyboardMarkup:
|
|||
|
|
"""Клавиатура выбора мини-игры"""
|
|||
|
|
return InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⚡ {t(lang, 'minigames.speed_round.name')}",
|
|||
|
|
callback_data="minigame_speed_round"
|
|||
|
|
)],
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"🎯 {t(lang, 'minigames.match_game.name')}",
|
|||
|
|
callback_data="minigame_match"
|
|||
|
|
)],
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_speed_round_start_keyboard(lang: str, is_premium: bool = False) -> InlineKeyboardMarkup:
|
|||
|
|
"""Клавиатура начала игры Speed Round"""
|
|||
|
|
buttons = []
|
|||
|
|
|
|||
|
|
if is_premium:
|
|||
|
|
# Для премиум-пользователей - выбор режима проверки
|
|||
|
|
buttons.append([
|
|||
|
|
InlineKeyboardButton(
|
|||
|
|
text=f"🤖 {t(lang, 'minigames.speed_round.mode_ai')}",
|
|||
|
|
callback_data="speed_round_start_ai"
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
buttons.append([
|
|||
|
|
InlineKeyboardButton(
|
|||
|
|
text=f"⚡ {t(lang, 'minigames.speed_round.mode_simple')}",
|
|||
|
|
callback_data="speed_round_start_simple"
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
else:
|
|||
|
|
# Для обычных пользователей - только простой режим
|
|||
|
|
buttons.append([
|
|||
|
|
InlineKeyboardButton(
|
|||
|
|
text=f"▶️ {t(lang, 'minigames.start_btn')}",
|
|||
|
|
callback_data="speed_round_start_simple"
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
buttons.append([
|
|||
|
|
InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
return InlineKeyboardMarkup(inline_keyboard=buttons)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def create_progress_bar(remaining: int, total: int = SPEED_ROUND_TIME) -> str:
|
|||
|
|
"""Создать прогресс-бар для таймера"""
|
|||
|
|
filled = int((remaining / total) * 10)
|
|||
|
|
empty = 10 - filled
|
|||
|
|
return "█" * filled + "░" * empty
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.message(Command("games"))
|
|||
|
|
async def cmd_games(message: Message, state: FSMContext):
|
|||
|
|
"""Команда /games - показать меню мини-игр"""
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, message.from_user.id)
|
|||
|
|
|
|||
|
|
if not user:
|
|||
|
|
await message.answer(t('ru', 'common.start_first'))
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lang = get_user_lang(user)
|
|||
|
|
|
|||
|
|
await message.answer(
|
|||
|
|
t(lang, 'minigames.menu_title'),
|
|||
|
|
reply_markup=get_minigames_menu_keyboard(lang)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "minigames_menu")
|
|||
|
|
async def minigames_menu_callback(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Показать меню мини-игр"""
|
|||
|
|
await callback.answer()
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
lang = get_user_lang(user) if user else 'ru'
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.menu_title'),
|
|||
|
|
reply_markup=get_minigames_menu_keyboard(lang)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "minigame_speed_round")
|
|||
|
|
async def speed_round_info(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Показать информацию о игре Speed Round"""
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
|
|||
|
|
if not user:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lang = get_user_lang(user)
|
|||
|
|
learning_lang = user.learning_language or 'en'
|
|||
|
|
|
|||
|
|
# Проверяем подписку с учётом даты окончания
|
|||
|
|
is_premium = getattr(user, 'is_premium', False)
|
|||
|
|
premium_until = getattr(user, 'premium_until', None)
|
|||
|
|
if is_premium and premium_until and premium_until < datetime.now():
|
|||
|
|
is_premium = False # Подписка истекла
|
|||
|
|
|
|||
|
|
# Проверяем, есть ли слова в словаре (для изучаемого языка)
|
|||
|
|
word_count = await VocabularyService.get_words_count(session, user.id, learning_lang=learning_lang)
|
|||
|
|
|
|||
|
|
if word_count < 5:
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.speed_round.not_enough_words', min=5, current=word_count),
|
|||
|
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Показываем правила игры
|
|||
|
|
rules_text = t(lang, 'minigames.speed_round.rules',
|
|||
|
|
count=SPEED_ROUND_COUNT,
|
|||
|
|
time=SPEED_ROUND_TIME)
|
|||
|
|
|
|||
|
|
# Добавляем информацию о режимах для премиум-пользователей
|
|||
|
|
if is_premium:
|
|||
|
|
rules_text += f"\n\n✨ {t(lang, 'minigames.speed_round.premium_hint')}"
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
f"⚡ <b>{t(lang, 'minigames.speed_round.name')}</b>\n\n{rules_text}",
|
|||
|
|
reply_markup=get_speed_round_start_keyboard(lang, is_premium)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "speed_round_start_ai")
|
|||
|
|
async def speed_round_ai_warning(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Показать предупреждение о работе AI режима"""
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
lang = get_user_lang(user) if user else 'ru'
|
|||
|
|
|
|||
|
|
# Показываем предупреждение о режиме AI
|
|||
|
|
warning_text = t(lang, 'minigames.speed_round.ai_mode_warning')
|
|||
|
|
|
|||
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"✅ {t(lang, 'minigames.start_btn')}",
|
|||
|
|
callback_data="speed_round_start_ai_confirm"
|
|||
|
|
)],
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigame_speed_round"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(warning_text, reply_markup=keyboard)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data.in_({"speed_round_start", "speed_round_start_simple", "speed_round_start_ai_confirm"}))
|
|||
|
|
async def speed_round_start(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Начать игру Speed Round"""
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
# Определяем режим проверки
|
|||
|
|
use_ai_check = callback.data == "speed_round_start_ai_confirm"
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
|
|||
|
|
if not user:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lang = get_user_lang(user)
|
|||
|
|
learning_lang = user.learning_language or 'en'
|
|||
|
|
translation_lang = user.translation_language or user.language_interface or 'ru'
|
|||
|
|
|
|||
|
|
# Получаем слова из словаря пользователя со всеми переводами
|
|||
|
|
game_words = await VocabularyService.get_random_words_with_translations(
|
|||
|
|
session, user.id, count=SPEED_ROUND_COUNT, learning_lang=learning_lang
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if len(game_words) < 5:
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.speed_round.not_enough_words', min=5, current=len(game_words))
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
await state.update_data(
|
|||
|
|
game_words=game_words,
|
|||
|
|
current_round=0,
|
|||
|
|
score=0,
|
|||
|
|
correct_count=0,
|
|||
|
|
results=[], # Список результатов по раундам
|
|||
|
|
user_id=user.id,
|
|||
|
|
lang=lang,
|
|||
|
|
learning_lang=learning_lang,
|
|||
|
|
translation_lang=translation_lang,
|
|||
|
|
use_ai_check=use_ai_check, # Режим проверки
|
|||
|
|
round_start_time=None,
|
|||
|
|
message_id=None
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await state.set_state(SpeedRoundStates.playing)
|
|||
|
|
|
|||
|
|
# Удаляем старое сообщение и начинаем игру
|
|||
|
|
await callback.message.delete()
|
|||
|
|
await show_speed_round_word(callback.message, state)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def show_speed_round_word(message: Message, state: FSMContext):
|
|||
|
|
"""Показать текущее слово в игре"""
|
|||
|
|
data = await state.get_data()
|
|||
|
|
game_words = data.get('game_words', [])
|
|||
|
|
current_round = data.get('current_round', 0)
|
|||
|
|
score = data.get('score', 0)
|
|||
|
|
lang = data.get('lang', 'ru')
|
|||
|
|
|
|||
|
|
if current_round >= len(game_words):
|
|||
|
|
# Игра окончена
|
|||
|
|
await finish_speed_round(message, state)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
word_data = game_words[current_round]
|
|||
|
|
word = word_data['word']
|
|||
|
|
transcription = word_data.get('transcription', '')
|
|||
|
|
|
|||
|
|
# Формируем текст
|
|||
|
|
word_display = f"<code>{word}</code>"
|
|||
|
|
if transcription:
|
|||
|
|
word_display += f"\n<i>[{transcription}]</i>"
|
|||
|
|
|
|||
|
|
text = (
|
|||
|
|
f"⚡ <b>{t(lang, 'minigames.speed_round.round', current=current_round + 1, total=len(game_words))}</b>\n\n"
|
|||
|
|
f"{word_display}\n\n"
|
|||
|
|
f"⏱ {create_progress_bar(SPEED_ROUND_TIME)} {SPEED_ROUND_TIME} {t(lang, 'minigames.speed_round.seconds')}\n\n"
|
|||
|
|
f"🏆 {t(lang, 'minigames.speed_round.score')}: <b>{score}</b>"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Сохраняем время начала раунда
|
|||
|
|
await state.update_data(
|
|||
|
|
round_start_time=datetime.now().timestamp()
|
|||
|
|
)
|
|||
|
|
await state.set_state(SpeedRoundStates.waiting_answer)
|
|||
|
|
|
|||
|
|
# Отправляем сообщение
|
|||
|
|
sent_msg = await message.answer(text)
|
|||
|
|
await state.update_data(message_id=sent_msg.message_id, chat_id=message.chat.id)
|
|||
|
|
|
|||
|
|
# Запускаем таймер
|
|||
|
|
asyncio.create_task(speed_round_timer(message.bot, state, sent_msg.chat.id, sent_msg.message_id))
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def speed_round_timer(bot, state: FSMContext, chat_id: int, message_id: int):
|
|||
|
|
"""Таймер для раунда с обновлением прогресс-бара"""
|
|||
|
|
for remaining in range(SPEED_ROUND_TIME - 1, -1, -1):
|
|||
|
|
await asyncio.sleep(1)
|
|||
|
|
|
|||
|
|
# Проверяем, всё ещё ли мы в состоянии ожидания ответа
|
|||
|
|
current_state = await state.get_state()
|
|||
|
|
if current_state != SpeedRoundStates.waiting_answer:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
data = await state.get_data()
|
|||
|
|
if data.get('message_id') != message_id:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
game_words = data.get('game_words', [])
|
|||
|
|
current_round = data.get('current_round', 0)
|
|||
|
|
score = data.get('score', 0)
|
|||
|
|
lang = data.get('lang', 'ru')
|
|||
|
|
|
|||
|
|
if current_round >= len(game_words):
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
word_data = game_words[current_round]
|
|||
|
|
word = word_data['word']
|
|||
|
|
transcription = word_data.get('transcription', '')
|
|||
|
|
|
|||
|
|
word_display = f"<code>{word}</code>"
|
|||
|
|
if transcription:
|
|||
|
|
word_display += f"\n<i>[{transcription}]</i>"
|
|||
|
|
|
|||
|
|
if remaining > 0:
|
|||
|
|
# Обновляем прогресс-бар
|
|||
|
|
text = (
|
|||
|
|
f"⚡ <b>{t(lang, 'minigames.speed_round.round', current=current_round + 1, total=len(game_words))}</b>\n\n"
|
|||
|
|
f"{word_display}\n\n"
|
|||
|
|
f"⏱ {create_progress_bar(remaining)} {remaining} {t(lang, 'minigames.speed_round.seconds')}\n\n"
|
|||
|
|
f"🏆 {t(lang, 'minigames.speed_round.score')}: <b>{score}</b>"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
await bot.edit_message_text(text, chat_id=chat_id, message_id=message_id)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
else:
|
|||
|
|
# Время вышло
|
|||
|
|
await state.set_state(SpeedRoundStates.playing)
|
|||
|
|
|
|||
|
|
# Сохраняем результат раунда (время вышло)
|
|||
|
|
results = data.get('results', [])
|
|||
|
|
results.append({
|
|||
|
|
'word': word_data['word'],
|
|||
|
|
'correct_answer': word_data['translation'],
|
|||
|
|
'user_answer': None,
|
|||
|
|
'is_correct': False
|
|||
|
|
})
|
|||
|
|
await state.update_data(current_round=current_round + 1, results=results)
|
|||
|
|
|
|||
|
|
# Показываем правильный ответ
|
|||
|
|
try:
|
|||
|
|
await bot.edit_message_text(
|
|||
|
|
f"⏰ <b>{t(lang, 'minigames.speed_round.time_up')}</b>\n\n"
|
|||
|
|
f"{word_display}\n\n"
|
|||
|
|
f"✅ {t(lang, 'minigames.speed_round.correct_was')}: <b>{word_data['translation']}</b>",
|
|||
|
|
chat_id=chat_id,
|
|||
|
|
message_id=message_id
|
|||
|
|
)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
await asyncio.sleep(1.5)
|
|||
|
|
|
|||
|
|
# Следующий раунд
|
|||
|
|
try:
|
|||
|
|
msg = await bot.send_message(chat_id, "...")
|
|||
|
|
await msg.delete()
|
|||
|
|
# Создаём фиктивный Message для продолжения
|
|||
|
|
from aiogram.types import Chat, User as TgUser
|
|||
|
|
fake_message = Message(
|
|||
|
|
message_id=0,
|
|||
|
|
date=datetime.now(),
|
|||
|
|
chat=Chat(id=chat_id, type="private"),
|
|||
|
|
from_user=None,
|
|||
|
|
text=""
|
|||
|
|
)
|
|||
|
|
fake_message._bot = bot
|
|||
|
|
await show_speed_round_word(fake_message, state)
|
|||
|
|
except Exception as e:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.message(SpeedRoundStates.waiting_answer)
|
|||
|
|
async def speed_round_answer(message: Message, state: FSMContext):
|
|||
|
|
"""Обработка ответа в игре Speed Round"""
|
|||
|
|
user_answer = message.text.strip()
|
|||
|
|
|
|||
|
|
data = await state.get_data()
|
|||
|
|
game_words = data.get('game_words', [])
|
|||
|
|
current_round = data.get('current_round', 0)
|
|||
|
|
score = data.get('score', 0)
|
|||
|
|
correct_count = data.get('correct_count', 0)
|
|||
|
|
lang = data.get('lang', 'ru')
|
|||
|
|
round_start_time = data.get('round_start_time', datetime.now().timestamp())
|
|||
|
|
old_message_id = data.get('message_id')
|
|||
|
|
chat_id = data.get('chat_id')
|
|||
|
|
use_ai_check = data.get('use_ai_check', False)
|
|||
|
|
user_id = data.get('user_id')
|
|||
|
|
learning_lang = data.get('learning_lang', 'en')
|
|||
|
|
translation_lang = data.get('translation_lang', 'ru')
|
|||
|
|
|
|||
|
|
if current_round >= len(game_words):
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
word_data = game_words[current_round]
|
|||
|
|
correct_answer = word_data['translation']
|
|||
|
|
|
|||
|
|
# Вычисляем время ответа
|
|||
|
|
time_taken = datetime.now().timestamp() - round_start_time
|
|||
|
|
time_remaining = max(0, SPEED_ROUND_TIME - time_taken)
|
|||
|
|
|
|||
|
|
# Переключаем состояние чтобы таймер остановился
|
|||
|
|
await state.set_state(SpeedRoundStates.playing)
|
|||
|
|
|
|||
|
|
# Простая проверка по всем вариантам перевода из word_translations
|
|||
|
|
# AI проверка будет в конце игры для всех ответов сразу
|
|||
|
|
user_answer_lower = user_answer.lower().strip()
|
|||
|
|
all_translations = word_data.get('all_translations', [correct_answer.lower()])
|
|||
|
|
|
|||
|
|
is_correct = False
|
|||
|
|
for valid_translation in all_translations:
|
|||
|
|
# Проверяем точное совпадение или вхождение
|
|||
|
|
if (user_answer_lower == valid_translation or
|
|||
|
|
user_answer_lower in valid_translation.split(',') or
|
|||
|
|
valid_translation in user_answer_lower):
|
|||
|
|
is_correct = True
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
# Получаем текущие результаты
|
|||
|
|
results = data.get('results', [])
|
|||
|
|
|
|||
|
|
if is_correct:
|
|||
|
|
# Начисляем очки
|
|||
|
|
round_points = POINTS_CORRECT + int(time_remaining * POINTS_SPEED_BONUS)
|
|||
|
|
score += round_points
|
|||
|
|
correct_count += 1
|
|||
|
|
|
|||
|
|
result_text = (
|
|||
|
|
f"✅ <b>{t(lang, 'minigames.speed_round.correct')}</b>\n\n"
|
|||
|
|
f"➕ {round_points} {t(lang, 'minigames.speed_round.points')}\n"
|
|||
|
|
f"⏱ {time_taken:.1f} {t(lang, 'minigames.speed_round.seconds')}"
|
|||
|
|
)
|
|||
|
|
else:
|
|||
|
|
result_text = (
|
|||
|
|
f"❌ <b>{t(lang, 'minigames.speed_round.wrong')}</b>\n\n"
|
|||
|
|
f"✅ {t(lang, 'minigames.speed_round.correct_was')}: <b>{correct_answer}</b>"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Сохраняем результат раунда (AI проверка будет в конце)
|
|||
|
|
results.append({
|
|||
|
|
'word': word_data['word'],
|
|||
|
|
'correct_answer': correct_answer,
|
|||
|
|
'user_answer': user_answer,
|
|||
|
|
'is_correct': is_correct, # Предварительный результат
|
|||
|
|
'time_remaining': time_remaining # Сохраняем для пересчёта очков
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Обновляем состояние
|
|||
|
|
await state.update_data(
|
|||
|
|
current_round=current_round + 1,
|
|||
|
|
score=score,
|
|||
|
|
correct_count=correct_count,
|
|||
|
|
results=results
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Удаляем старое сообщение с вопросом
|
|||
|
|
try:
|
|||
|
|
await message.bot.delete_message(chat_id, old_message_id)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Показываем результат
|
|||
|
|
await message.answer(result_text)
|
|||
|
|
await asyncio.sleep(1)
|
|||
|
|
|
|||
|
|
# Следующий раунд
|
|||
|
|
await show_speed_round_word(message, state)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def finish_speed_round(message: Message, state: FSMContext):
|
|||
|
|
"""Завершение игры Speed Round"""
|
|||
|
|
data = await state.get_data()
|
|||
|
|
score = data.get('score', 0)
|
|||
|
|
correct_count = data.get('correct_count', 0)
|
|||
|
|
game_words = data.get('game_words', [])
|
|||
|
|
results = data.get('results', [])
|
|||
|
|
lang = data.get('lang', 'ru')
|
|||
|
|
use_ai_check = data.get('use_ai_check', False)
|
|||
|
|
user_id = data.get('user_id')
|
|||
|
|
learning_lang = data.get('learning_lang', 'en')
|
|||
|
|
translation_lang = data.get('translation_lang', 'ru')
|
|||
|
|
total = len(game_words)
|
|||
|
|
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
# AI проверка в конце игры (один запрос для всех ответов)
|
|||
|
|
if use_ai_check and results:
|
|||
|
|
# Отправляем сообщение о проверке
|
|||
|
|
checking_msg = await message.answer(f"🤖 {t(lang, 'minigames.speed_round.ai_checking')}...")
|
|||
|
|
|
|||
|
|
# Собираем ответы для проверки (только те, где пользователь ответил)
|
|||
|
|
answers_to_check = [
|
|||
|
|
{
|
|||
|
|
'word': r['word'],
|
|||
|
|
'correct_translation': r['correct_answer'],
|
|||
|
|
'user_answer': r['user_answer']
|
|||
|
|
}
|
|||
|
|
for r in results if r['user_answer']
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
if answers_to_check:
|
|||
|
|
# Проверяем все ответы одним запросом
|
|||
|
|
ai_results = await ai_service.check_translations_batch(
|
|||
|
|
answers=answers_to_check,
|
|||
|
|
source_lang=learning_lang,
|
|||
|
|
target_lang=translation_lang,
|
|||
|
|
user_id=user_id
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Обновляем результаты и пересчитываем очки
|
|||
|
|
ai_idx = 0
|
|||
|
|
score = 0
|
|||
|
|
correct_count = 0
|
|||
|
|
|
|||
|
|
for result in results:
|
|||
|
|
if result['user_answer']:
|
|||
|
|
# Обновляем результат из AI проверки
|
|||
|
|
ai_result = ai_results[ai_idx]
|
|||
|
|
result['is_correct'] = ai_result['is_correct']
|
|||
|
|
result['ai_feedback'] = ai_result.get('feedback', '')
|
|||
|
|
result['user_answer_meaning'] = ai_result.get('user_answer_meaning', '')
|
|||
|
|
ai_idx += 1
|
|||
|
|
|
|||
|
|
if result['is_correct']:
|
|||
|
|
# Пересчитываем очки
|
|||
|
|
time_remaining = result.get('time_remaining', 0)
|
|||
|
|
round_points = POINTS_CORRECT + int(time_remaining * POINTS_SPEED_BONUS)
|
|||
|
|
score += round_points
|
|||
|
|
correct_count += 1
|
|||
|
|
else:
|
|||
|
|
# Время вышло - ответ неверный
|
|||
|
|
result['is_correct'] = False
|
|||
|
|
result['ai_feedback'] = ''
|
|||
|
|
result['user_answer_meaning'] = ''
|
|||
|
|
|
|||
|
|
# Удаляем сообщение о проверке
|
|||
|
|
try:
|
|||
|
|
await checking_msg.delete()
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Определяем результат
|
|||
|
|
accuracy = int((correct_count / total) * 100) if total > 0 else 0
|
|||
|
|
|
|||
|
|
if accuracy >= 90:
|
|||
|
|
emoji = "🏆"
|
|||
|
|
comment = t(lang, 'minigames.speed_round.result.excellent')
|
|||
|
|
elif accuracy >= 70:
|
|||
|
|
emoji = "🎉"
|
|||
|
|
comment = t(lang, 'minigames.speed_round.result.good')
|
|||
|
|
elif accuracy >= 50:
|
|||
|
|
emoji = "👍"
|
|||
|
|
comment = t(lang, 'minigames.speed_round.result.average')
|
|||
|
|
else:
|
|||
|
|
emoji = "💪"
|
|||
|
|
comment = t(lang, 'minigames.speed_round.result.practice')
|
|||
|
|
|
|||
|
|
# Формируем список ответов
|
|||
|
|
answers_text = ""
|
|||
|
|
for result in results:
|
|||
|
|
if result['is_correct']:
|
|||
|
|
answers_text += f"✅ {result['word']} → {result['correct_answer']}"
|
|||
|
|
if use_ai_check and result.get('ai_feedback'):
|
|||
|
|
answers_text += f" <i>({result['ai_feedback']})</i>"
|
|||
|
|
answers_text += "\n"
|
|||
|
|
else:
|
|||
|
|
user_ans = result['user_answer'] or "⏰"
|
|||
|
|
answers_text += f"❌ {result['word']} → <s>{user_ans}</s>"
|
|||
|
|
# Показываем значение ответа пользователя (что он на самом деле написал)
|
|||
|
|
if use_ai_check and result.get('user_answer_meaning'):
|
|||
|
|
answers_text += f" <i>= {result['user_answer_meaning']}</i>"
|
|||
|
|
answers_text += f" ({result['correct_answer']})"
|
|||
|
|
if use_ai_check and result.get('ai_feedback'):
|
|||
|
|
answers_text += f" <i>({result['ai_feedback']})</i>"
|
|||
|
|
answers_text += "\n"
|
|||
|
|
|
|||
|
|
# Добавляем пометку об AI проверке
|
|||
|
|
ai_badge = "🤖 " if use_ai_check else ""
|
|||
|
|
|
|||
|
|
text = (
|
|||
|
|
f"{emoji} <b>{t(lang, 'minigames.speed_round.finished')}</b>\n\n"
|
|||
|
|
f"🏆 {t(lang, 'minigames.speed_round.final_score')}: <b>{score}</b>\n"
|
|||
|
|
f"✅ {t(lang, 'minigames.speed_round.correct_answers')}: <b>{correct_count}/{total}</b>\n"
|
|||
|
|
f"🎯 {t(lang, 'minigames.speed_round.accuracy')}: <b>{accuracy}%</b>\n\n"
|
|||
|
|
f"📋 {ai_badge}<b>{t(lang, 'minigames.speed_round.answers_list')}:</b>\n"
|
|||
|
|
f"{answers_text}\n"
|
|||
|
|
f"{comment}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"🔄 {t(lang, 'minigames.play_again')}",
|
|||
|
|
callback_data="speed_round_start"
|
|||
|
|
)],
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)],
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
await message.answer(text, reply_markup=keyboard)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ==================== MATCH GAME (Найди пару) ====================
|
|||
|
|
|
|||
|
|
def get_match_game_start_keyboard(lang: str) -> InlineKeyboardMarkup:
|
|||
|
|
"""Клавиатура начала игры Match"""
|
|||
|
|
return InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"▶️ {t(lang, 'minigames.start_btn')}",
|
|||
|
|
callback_data="match_game_start"
|
|||
|
|
)],
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "minigame_match")
|
|||
|
|
async def match_game_info(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Показать информацию о игре Match"""
|
|||
|
|
await callback.answer()
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
|
|||
|
|
if not user:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lang = get_user_lang(user)
|
|||
|
|
learning_lang = user.learning_language or 'en'
|
|||
|
|
|
|||
|
|
# Проверяем, есть ли слова в словаре
|
|||
|
|
word_count = await VocabularyService.get_words_count(session, user.id, learning_lang=learning_lang)
|
|||
|
|
|
|||
|
|
if word_count < MATCH_WORDS_COUNT:
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.match_game.not_enough_words', min=MATCH_WORDS_COUNT, current=word_count),
|
|||
|
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Показываем правила игры
|
|||
|
|
rules_text = t(lang, 'minigames.match_game.rules', count=MATCH_WORDS_COUNT)
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
f"🎯 <b>{t(lang, 'minigames.match_game.name')}</b>\n\n{rules_text}",
|
|||
|
|
reply_markup=get_match_game_start_keyboard(lang)
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "match_game_start")
|
|||
|
|
async def match_game_start(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Начать игру Match"""
|
|||
|
|
await callback.answer()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
|
|||
|
|
if not user:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
lang = get_user_lang(user)
|
|||
|
|
learning_lang = user.learning_language or 'en'
|
|||
|
|
|
|||
|
|
# Получаем случайные слова из словаря
|
|||
|
|
game_words = await VocabularyService.get_random_words_with_translations(
|
|||
|
|
session, user.id, count=MATCH_WORDS_COUNT, learning_lang=learning_lang
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if len(game_words) < MATCH_WORDS_COUNT:
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.match_game.not_enough_words', min=MATCH_WORDS_COUNT, current=len(game_words))
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Создаём перемешанные списки
|
|||
|
|
words_list = [(i + 1, w['word']) for i, w in enumerate(game_words)] # (номер, слово)
|
|||
|
|
translations_list = [(chr(65 + i), w['translation']) for i, w in enumerate(game_words)] # (буква, перевод)
|
|||
|
|
|
|||
|
|
# Перемешиваем переводы
|
|||
|
|
random.shuffle(translations_list)
|
|||
|
|
|
|||
|
|
# Создаём правильные ответы (номер -> буква)
|
|||
|
|
correct_pairs = {}
|
|||
|
|
for i, word_data in enumerate(game_words):
|
|||
|
|
word_num = i + 1
|
|||
|
|
translation = word_data['translation']
|
|||
|
|
# Находим букву перевода в перемешанном списке
|
|||
|
|
for letter, trans in translations_list:
|
|||
|
|
if trans == translation:
|
|||
|
|
correct_pairs[word_num] = letter
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
await state.update_data(
|
|||
|
|
game_words=game_words,
|
|||
|
|
words_list=words_list,
|
|||
|
|
translations_list=translations_list,
|
|||
|
|
correct_pairs=correct_pairs,
|
|||
|
|
user_id=user.id,
|
|||
|
|
lang=lang,
|
|||
|
|
start_time=datetime.now().timestamp()
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
await state.set_state(MatchGameStates.waiting_answer)
|
|||
|
|
|
|||
|
|
# Формируем текст игры
|
|||
|
|
game_text = format_match_game_text(lang, words_list, translations_list)
|
|||
|
|
|
|||
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"❌ {t(lang, 'minigames.match_game.cancel')}",
|
|||
|
|
callback_data="match_game_cancel"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(game_text, reply_markup=keyboard)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def format_match_game_text(lang: str, words_list: list, translations_list: list) -> str:
|
|||
|
|
"""Форматировать текст игры Match"""
|
|||
|
|
text = f"🎯 <b>{t(lang, 'minigames.match_game.title')}</b>\n\n"
|
|||
|
|
|
|||
|
|
# Формируем две колонки
|
|||
|
|
text += f"<b>{t(lang, 'minigames.match_game.words_col')}:</b>\n"
|
|||
|
|
for num, word in words_list:
|
|||
|
|
text += f" {num}. {word}\n"
|
|||
|
|
|
|||
|
|
text += f"\n<b>{t(lang, 'minigames.match_game.translations_col')}:</b>\n"
|
|||
|
|
for letter, translation in translations_list:
|
|||
|
|
text += f" {letter}. {translation}\n"
|
|||
|
|
|
|||
|
|
text += f"\n💡 {t(lang, 'minigames.match_game.hint')}"
|
|||
|
|
|
|||
|
|
return text
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.callback_query(F.data == "match_game_cancel")
|
|||
|
|
async def match_game_cancel(callback: CallbackQuery, state: FSMContext):
|
|||
|
|
"""Отменить игру Match"""
|
|||
|
|
await callback.answer()
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
async with async_session_maker() as session:
|
|||
|
|
user = await UserService.get_user_by_telegram_id(session, callback.from_user.id)
|
|||
|
|
lang = get_user_lang(user) if user else 'ru'
|
|||
|
|
|
|||
|
|
await callback.message.edit_text(
|
|||
|
|
t(lang, 'minigames.match_game.cancelled'),
|
|||
|
|
reply_markup=InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)]
|
|||
|
|
])
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.message(MatchGameStates.waiting_answer)
|
|||
|
|
async def match_game_answer(message: Message, state: FSMContext):
|
|||
|
|
"""Обработка ответа в игре Match"""
|
|||
|
|
user_input = message.text.strip().upper()
|
|||
|
|
|
|||
|
|
data = await state.get_data()
|
|||
|
|
correct_pairs = data.get('correct_pairs', {})
|
|||
|
|
words_list = data.get('words_list', [])
|
|||
|
|
translations_list = data.get('translations_list', [])
|
|||
|
|
game_words = data.get('game_words', [])
|
|||
|
|
lang = data.get('lang', 'ru')
|
|||
|
|
start_time = data.get('start_time', datetime.now().timestamp())
|
|||
|
|
|
|||
|
|
# Парсим ответ пользователя
|
|||
|
|
# Ожидаемый формат: 1A, 2B, 3C или 1A 2B 3C или 1-A, 2-B
|
|||
|
|
import re
|
|||
|
|
pairs_pattern = r'(\d+)\s*[-]?\s*([A-Z])'
|
|||
|
|
user_pairs_raw = re.findall(pairs_pattern, user_input)
|
|||
|
|
|
|||
|
|
# Преобразуем в словарь
|
|||
|
|
user_pairs = {}
|
|||
|
|
for num_str, letter in user_pairs_raw:
|
|||
|
|
num = int(num_str)
|
|||
|
|
if 1 <= num <= len(words_list):
|
|||
|
|
user_pairs[num] = letter
|
|||
|
|
|
|||
|
|
# Проверяем, достаточно ли пар введено
|
|||
|
|
if len(user_pairs) < len(correct_pairs):
|
|||
|
|
await message.answer(
|
|||
|
|
t(lang, 'minigames.match_game.not_all_pairs',
|
|||
|
|
entered=len(user_pairs),
|
|||
|
|
needed=len(correct_pairs))
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# Завершаем игру
|
|||
|
|
await state.clear()
|
|||
|
|
|
|||
|
|
# Считаем время
|
|||
|
|
time_taken = datetime.now().timestamp() - start_time
|
|||
|
|
|
|||
|
|
# Проверяем ответы
|
|||
|
|
correct_count = 0
|
|||
|
|
results = []
|
|||
|
|
|
|||
|
|
for num, letter in correct_pairs.items():
|
|||
|
|
word_data = game_words[num - 1]
|
|||
|
|
user_letter = user_pairs.get(num, '?')
|
|||
|
|
is_correct = user_letter == letter
|
|||
|
|
|
|||
|
|
if is_correct:
|
|||
|
|
correct_count += 1
|
|||
|
|
|
|||
|
|
results.append({
|
|||
|
|
'num': num,
|
|||
|
|
'word': word_data['word'],
|
|||
|
|
'translation': word_data['translation'],
|
|||
|
|
'correct_letter': letter,
|
|||
|
|
'user_letter': user_letter,
|
|||
|
|
'is_correct': is_correct
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# Вычисляем очки
|
|||
|
|
score = correct_count * MATCH_POINTS_PER_PAIR
|
|||
|
|
total = len(correct_pairs)
|
|||
|
|
accuracy = int((correct_count / total) * 100) if total > 0 else 0
|
|||
|
|
|
|||
|
|
# Определяем результат
|
|||
|
|
if accuracy == 100:
|
|||
|
|
emoji = "🏆"
|
|||
|
|
comment = t(lang, 'minigames.match_game.result.perfect')
|
|||
|
|
elif accuracy >= 80:
|
|||
|
|
emoji = "🎉"
|
|||
|
|
comment = t(lang, 'minigames.match_game.result.excellent')
|
|||
|
|
elif accuracy >= 60:
|
|||
|
|
emoji = "👍"
|
|||
|
|
comment = t(lang, 'minigames.match_game.result.good')
|
|||
|
|
else:
|
|||
|
|
emoji = "💪"
|
|||
|
|
comment = t(lang, 'minigames.match_game.result.practice')
|
|||
|
|
|
|||
|
|
# Формируем список результатов
|
|||
|
|
results_text = ""
|
|||
|
|
for r in results:
|
|||
|
|
if r['is_correct']:
|
|||
|
|
results_text += f"✅ {r['num']}. {r['word']} → {r['correct_letter']}. {r['translation']}\n"
|
|||
|
|
else:
|
|||
|
|
results_text += f"❌ {r['num']}. {r['word']} → <s>{r['user_letter']}</s> ({r['correct_letter']}. {r['translation']})\n"
|
|||
|
|
|
|||
|
|
text = (
|
|||
|
|
f"{emoji} <b>{t(lang, 'minigames.match_game.finished')}</b>\n\n"
|
|||
|
|
f"🏆 {t(lang, 'minigames.match_game.score')}: <b>{score}</b>\n"
|
|||
|
|
f"✅ {t(lang, 'minigames.match_game.correct_pairs')}: <b>{correct_count}/{total}</b>\n"
|
|||
|
|
f"⏱ {t(lang, 'minigames.match_game.time')}: <b>{time_taken:.1f}</b> {t(lang, 'minigames.speed_round.seconds')}\n\n"
|
|||
|
|
f"📋 <b>{t(lang, 'minigames.match_game.results')}:</b>\n"
|
|||
|
|
f"{results_text}\n"
|
|||
|
|
f"{comment}"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"🔄 {t(lang, 'minigames.play_again')}",
|
|||
|
|
callback_data="match_game_start"
|
|||
|
|
)],
|
|||
|
|
[InlineKeyboardButton(
|
|||
|
|
text=f"⬅️ {t(lang, 'minigames.back_btn')}",
|
|||
|
|
callback_data="minigames_menu"
|
|||
|
|
)],
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
await message.answer(text, reply_markup=keyboard)
|