Files
tg_bot_language/bot/handlers/minigames.py
mamonov.ep adc8a6bf8e feat: мини-игры, premium подписка, улучшенные контексты
Мини-игры (/games):
- Speed Round: 10 раундов, 10 секунд на ответ, очки за скорость
- Match Pairs: 5 слов + 5 переводов, соединить пары

Premium-функции:
- Поля is_premium и premium_until для пользователей
- AI режим проверки ответов (учитывает синонимы)
- Batch проверка всех ответов одним запросом

Улучшения:
- Примеры использования для всех добавляемых слов
- Разбиение переводов по запятой на отдельные записи
- Полные предложения в контекстах (без ___)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-10 19:42:10 +03:00

904 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Обработчики мини-игр"""
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)