diff --git a/poetry.lock b/poetry.lock index c6074f8..9488111 100644 --- a/poetry.lock +++ b/poetry.lock @@ -621,6 +621,24 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "networkx" +version = "3.2.1" +description = "Python package for creating and manipulating graphs and networks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "networkx-3.2.1-py3-none-any.whl", hash = "sha256:f18c69adc97877c42332c170849c96cefa91881c99a7cb3e95b7c659ebdc1ec2"}, + {file = "networkx-3.2.1.tar.gz", hash = "sha256:9f1bb5cf3409bf324e0a722c20bdb4c20ee39bf1c30ce8ae499c8502b0b5e0c6"}, +] + +[package.extras] +default = ["matplotlib (>=3.5)", "numpy (>=1.22)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"] +developer = ["changelist (==0.4)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"] +doc = ["nb2plots (>=0.7)", "nbconvert (<7.9)", "numpydoc (>=1.6)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"] +extra = ["lxml (>=4.6)", "pydot (>=1.4.2)", "pygraphviz (>=1.11)", "sympy (>=1.10)"] +test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] + [[package]] name = "nodeenv" version = "1.8.0" @@ -1093,4 +1111,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "74b5d10c672b860dada4f42ba97679cbab3b3e9b29d92a9ff7fbed7f61bfd5c5" +content-hash = "cd0c5b9b5963bfaaf67b54596050f1f7d860381ae6e360dc266ad14dc186d30c" diff --git a/pyproject.toml b/pyproject.toml index 99ae33b..eb82a5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ requests = "^2.31.0" types-requests = "^2.31.0.20240125" mypy = "^1.8.0" pyquizapi = "^0.0.14" +networkx = "^3.2.1" [tool.poetry.group.dev.dependencies] diff --git a/tg_bot/app.py b/tg_bot/app.py index 85d3cc6..678e4d1 100644 --- a/tg_bot/app.py +++ b/tg_bot/app.py @@ -16,12 +16,18 @@ from tg_bot.handlers import ( lesson_one, lesson_tree, lesson_two, + quest, + quiz, + tic_tac_toy, ) TOKEN: str = getenv('BOT_TOKEN') or 'Your TG_BOT token' dp = Dispatcher() dp.include_routers( + quest.router, + tic_tac_toy.router, + quiz.router, games.router, lesson_five.router, lesson_four.router, diff --git a/tg_bot/handlers/games.py b/tg_bot/handlers/games.py index f2adf78..ec73164 100644 --- a/tg_bot/handlers/games.py +++ b/tg_bot/handlers/games.py @@ -1,29 +1,6 @@ -from os import getenv - -from aiogram import Bot, F, Router +from aiogram import Router from aiogram.filters import Command -from aiogram.fsm.context import FSMContext -from aiogram.fsm.state import State, StatesGroup -from aiogram.types import ( - CallbackQuery, - InlineKeyboardButton, - InlineKeyboardMarkup, - KeyboardButton, - Message, - ReplyKeyboardMarkup, - ReplyKeyboardRemove, -) -from pyquizAPI import QuizClient - - -async def get_question(): - api = getenv('QUIZ_API') - client = QuizClient(api) - client.make_config(limit=1, difficulty='Easy') - - questions = client.get_questions(use_config=True) - return questions - +from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup router = Router() kb = [ @@ -33,87 +10,9 @@ kb = [ ] -class GameState(StatesGroup): - game = State() - quiz = State() - tic_tac_toy = State() - quest = State() - - markup_kb = ReplyKeyboardMarkup(keyboard=kb, resize_keyboard=True) @router.message(Command('games')) async def games(message: Message): await message.answer('Select Game:', reply_markup=markup_kb) - - -@router.message(F.text.lower() == 'exit game') -async def exit_games(message: Message, state: FSMContext): - await state.clear() - await message.reply('Game is finished', reply_markup=ReplyKeyboardRemove()) - - -@router.message(F.text.lower() == 'quiz') -async def with_puree(message: Message, state: FSMContext): - quiz_kb = [ - [KeyboardButton(text='Next question')], - [KeyboardButton(text='Exit Game')], - ] - await state.set_state(GameState.quiz) - - await message.reply('Start Quiz', reply_markup=ReplyKeyboardRemove()) - await message.reply( - 'First question:', - reply_markup=ReplyKeyboardMarkup(keyboard=quiz_kb, resize_keyboard=True), - ) - await question(message, state) - - -@router.message(GameState.quiz, F.text) -async def question(message: Message, state: FSMContext): - data = await state.get_data() - question_data = await get_question() - question = question_data[0].get('question') - answers = { - question_data[0]['answers'][k]: question_data[0]['correct_answers'][ - k + '_correct' - ] - for k in question_data[0]['answers'].keys() - if question_data[0]['answers'][k] is not None - } - buttons = [] - - idx = 1 - text = f'Question:\n{question}\n\nanswers:\n' - - for k, v in answers.items(): - buttons.append([InlineKeyboardButton(text=f'choice {idx}', callback_data=k)]) - if v == 'true': - data['answer'] = k - text = text + f'{idx} - {k}\n' - idx += 1 - - data['question'] = question - await state.update_data(data) - - await message.answer( - text=text, - reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), - ) - - -@router.callback_query(GameState.quiz) -async def check_answer(callback: CallbackQuery, state: FSMContext, bot: Bot): - answer = (await state.get_data())['answer'] - - await callback.answer() - if answer == callback.data: - await callback.message.answer(text='Correct') - else: - await callback.message.answer(text='Opps. You miss') - await bot.edit_message_text( - chat_id=callback.from_user.id, - message_id=callback.message.message_id, - text=callback.message.text, - ) diff --git a/tg_bot/handlers/quest.py b/tg_bot/handlers/quest.py new file mode 100644 index 0000000..f972328 --- /dev/null +++ b/tg_bot/handlers/quest.py @@ -0,0 +1,153 @@ +import networkx as nx +from aiogram import Bot, F, Router +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + KeyboardButton, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from aiogram.utils.formatting import Bold, as_list, as_marked_section + +G = nx.Graph() + +G.add_node('start') +G.add_node('A') +G.add_node('B') +G.add_node('C') +G.add_node('D') +G.add_node('end') + +G.add_edge('start', 'A') +G.add_edge('start', 'B') +G.add_edge('A', 'C') +G.add_edge('A', 'D') +G.add_edge('D', 'end') + +router = Router() + + +class GameState(StatesGroup): + quest = State() + + +@router.message(F.text.lower() == 'exit game') +async def exit_games(message: Message, state: FSMContext): + await state.clear() + await message.reply('Game is finished', reply_markup=ReplyKeyboardRemove()) + + +async def get_content(place, backward, forward): + if forward != ['None']: + f_text = 'Можете продолжить путь' + else: + f_text = 'Вы пришли в тупик, поищите другой путь' + + content = as_list( + as_marked_section( + Bold('Вы находитесь в локации:'), + place, + marker='*️⃣ ', + ), + as_marked_section( + Bold( + 'Вы можете вернуться назад в локацию' + if backward + else 'Вы в начале, пути назад нет' + ), + backward, + marker='⏪ ', + ), + as_marked_section( + Bold(f_text), + *forward, + marker='⏩ ', + ), + sep='\n\n', + ) + return content + + +async def get_keyboard(backward, forward): + buttons = [] + + if backward is not None: + buttons.append( + [ + InlineKeyboardButton( + text=f'◀️◀️◀️ Вернуться в {backward}', + callback_data=f'bacward_{backward}', + ) + ], + ) + if forward is not None: + for f in forward: + buttons.append( + [ + InlineKeyboardButton( + text=f'Идти в {f} ▶️▶️▶️', callback_data=f'forward_{f}' + ) + ], + ) + + keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) + return keyboard + + +@router.message(F.text.lower() == 'quest') +async def start_quest(message: Message, state: FSMContext): + quest_kb = [ + [KeyboardButton(text='Exit Game')], + ] + + await state.set_state(GameState.quest) + + data = {'backward': None, 'place': 'start', 'forward': ['A', 'B']} + + await state.set_data(data) + + await message.reply('Start quest', reply_markup=ReplyKeyboardRemove()) + await message.reply( + 'Make your move', + reply_markup=ReplyKeyboardMarkup(keyboard=quest_kb, resize_keyboard=True), + ) + await make_move(message, state) + + +@router.message(GameState.quest, F.text) +async def make_move(message: Message, state: FSMContext): + data = await state.get_data() + + keyboard = await get_keyboard(data['backward'], data['forward']) + content = await get_content(data['place'], data['backward'], data['forward']) + + await message.answer(**content.as_kwargs(), reply_markup=keyboard) + + +@router.callback_query(GameState.quest) +async def check_answer(callback: CallbackQuery, state: FSMContext, bot: Bot): + data = await state.get_data() + data['backward'] = data['place'] + data['place'] = (callback.data.split('_'))[1] + data['forward'] = list(G.neighbors(data['place'])) + data['forward'].remove(data['backward']) + + await state.update_data(data) + if data['place'] == 'end': + await callback.answer(text='Вы дошли до конца!!!', show_alert=True) + await start_quest(callback.message, state) + return + + content = await get_content( + data['place'], data['backward'], data['forward'] or ['None'] + ) + keyboard = await get_keyboard(data['backward'], data['forward']) + + await bot.send_message( + **content.as_kwargs(), chat_id=callback.from_user.id, reply_markup=keyboard + ) + await callback.answer() diff --git a/tg_bot/handlers/quiz.py b/tg_bot/handlers/quiz.py new file mode 100644 index 0000000..12d6930 --- /dev/null +++ b/tg_bot/handlers/quiz.py @@ -0,0 +1,142 @@ +from os import getenv + +from aiogram import Bot, F, Router, html +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + KeyboardButton, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from aiogram.utils.formatting import Bold, as_key_value, as_list, as_marked_section +from pyquizAPI import QuizClient + + +async def get_question(): + api = getenv('QUIZ_API') + client = QuizClient(api) + client.make_config(limit=1, difficulty='Easy') + + questions = client.get_questions(use_config=True) + return questions + + +router = Router() + + +class GameState(StatesGroup): + quiz = State() + + +@router.message(F.text.lower() == 'exit game') +async def exit_games(message: Message, state: FSMContext): + await state.clear() + await message.reply('Game is finished', reply_markup=ReplyKeyboardRemove()) + + +async def get_formated_question( + qnt: int, correct: int, question: str, answers: list[str] +): + content = as_list( + as_marked_section( + Bold('Game summary:'), + as_key_value('Total questions', qnt), + as_key_value('Success', correct), + as_key_value('Failed', qnt - correct), + marker=' ', + ), + as_marked_section( + Bold('Question:'), + html.quote(question), + marker='❔ ', + ), + as_marked_section( + Bold('Answers:'), + *[html.quote(item) for item in answers], + marker='➖ ', + ), + sep='\n\n', + ) + return content + + +@router.message(F.text.lower() == 'quiz') +async def start_quiz(message: Message, state: FSMContext): + quiz_kb = [ + [KeyboardButton(text='Next question')], + [KeyboardButton(text='Exit Game')], + ] + await state.set_state(GameState.quiz) + + await message.reply('Start Quiz', reply_markup=ReplyKeyboardRemove()) + await message.reply( + 'First question:', + reply_markup=ReplyKeyboardMarkup(keyboard=quiz_kb, resize_keyboard=True), + ) + await question(message, state) + + +@router.message(GameState.quiz, F.text) +async def question(message: Message, state: FSMContext): + data = await state.get_data() + question_data = await get_question() + question = question_data[0].get('question') + answers = { + question_data[0]['answers'][k]: question_data[0]['correct_answers'][ + k + '_correct' + ] + for k in question_data[0]['answers'].keys() + if question_data[0]['answers'][k] is not None + } + buttons = [] + + idx = 1 + + for k, v in answers.items(): + buttons.append([InlineKeyboardButton(text=f'choice {idx}', callback_data=k)]) + if v == 'true': + data['answer'] = k + idx += 1 + + content = await get_formated_question( + data.get('quantity', 0), data.get('score', 0), question, list(answers.keys()) + ) + data['question'] = question + await state.update_data(data) + + await message.answer( + **content.as_kwargs(), + reply_markup=InlineKeyboardMarkup(inline_keyboard=buttons), + ) + + +@router.callback_query(GameState.quiz) +async def check_answer(callback: CallbackQuery, state: FSMContext, bot: Bot): + data = await state.get_data() + answer = data['answer'] + score = data.get('score', 0) + qnt = data.get('quantity', 0) + + await callback.answer() + if answer == callback.data: + await callback.message.answer(text='Correct') + score += 1 + else: + await callback.message.answer(text='Opps. You miss') + qnt += 1 + + data['score'] = score + data['quantity'] = qnt + await state.update_data(data) + + msg_text: str = callback.message.text + + await bot.edit_message_text( + chat_id=callback.from_user.id, + message_id=callback.message.message_id, + text=msg_text, + ) diff --git a/tg_bot/handlers/tic_tac_toy.py b/tg_bot/handlers/tic_tac_toy.py new file mode 100644 index 0000000..fb2f78c --- /dev/null +++ b/tg_bot/handlers/tic_tac_toy.py @@ -0,0 +1,213 @@ +from random import choice + +from aiogram import Bot, F, Router +from aiogram.fsm.context import FSMContext +from aiogram.fsm.state import State, StatesGroup +from aiogram.types import ( + CallbackQuery, + InlineKeyboardButton, + InlineKeyboardMarkup, + KeyboardButton, + Message, + ReplyKeyboardMarkup, + ReplyKeyboardRemove, +) +from aiogram.utils.formatting import Bold, as_key_value, as_list, as_marked_section + +router = Router() + + +class GameState(StatesGroup): + tic_tac_toy = State() + + +@router.message(F.text.lower() == 'exit game') +async def exit_games(message: Message, state: FSMContext): + await state.clear() + await message.reply('Game is finished', reply_markup=ReplyKeyboardRemove()) + + +async def get_keyboard(field: list): + buttons = [] + + for x in range(3): + row = [] + for y in range(3): + callback_data = f'{x} {y}' + text = '' + + match field[x][y]: + case None: + text = '⬛️' + case False: + text = '⭕️' + case True: + text = '❌' + + row.append( + InlineKeyboardButton(text=text, callback_data=callback_data), + ) + buttons.append(row) + + keyboard = InlineKeyboardMarkup(inline_keyboard=buttons) + return keyboard + + +async def get_content(bot_wins, user_wins, move): + content = as_list( + as_marked_section( + Bold('Game summary:'), + as_key_value('Total games', bot_wins + user_wins), + as_key_value('user wins', user_wins), + as_key_value('bot wins', bot_wins), + as_key_value('Move: ', move), + marker=' ', + ), + sep='\n\n', + ) + return content + + +async def check_winner(field): + winner = None + + def check_equal_all_in_row(row): + if row[0] == row[1] and row[0] == row[2]: + return True + return False + + for row in field: + if check_equal_all_in_row(row): + winner = row[0] + + for row in zip(*field): + if check_equal_all_in_row(row): + winner = row[0] + + row = [] + for i in range(3): + row.append(field[i][i]) + + if check_equal_all_in_row(row): + winner = row[0] + + row = [] + for i in range(3): + row.append(field[i][2 - i]) + + if check_equal_all_in_row(row): + winner = row[0] + + return winner + + +@router.message(F.text.lower() == 'tic_tac_toy') +async def start_tic_tac_toy( + message: Message, state: FSMContext, user_wins=0, bot_wins=0 +): + tic_tac_toy_kb = [ + [KeyboardButton(text='Exit Game')], + ] + await state.clear() + await state.set_state(GameState.tic_tac_toy) + + data = { + 'field': [[None for _ in range(3)] for _ in range(3)], + 'availible_moves': [(x, y) for x in range(3) for y in range(3)], + 'user_wins': user_wins, + 'bot_wins': bot_wins, + 'move': 0, + } + + await state.set_data(data) + + await message.reply('Start Tic Tac Toy', reply_markup=ReplyKeyboardRemove()) + await message.reply( + 'Make your move', + reply_markup=ReplyKeyboardMarkup(keyboard=tic_tac_toy_kb, resize_keyboard=True), + ) + await make_move(message, state) + + +@router.message(GameState.tic_tac_toy, F.text) +async def make_move(message: Message, state: FSMContext): + data = await state.get_data() + keyboard = await get_keyboard(data['field']) + await state.update_data(data) + content = await get_content( + data['bot_wins'], + data['user_wins'], + data['move'], + ) + + await message.answer(**content.as_kwargs(), reply_markup=keyboard) + + +@router.callback_query(GameState.tic_tac_toy) +async def check_answer(callback: CallbackQuery, state: FSMContext, bot: Bot): + data = await state.get_data() + x, y = map(int, callback.data.split()) + if (x, y) not in data['availible_moves']: + await callback.answer(text='Cell is busy', show_alert=True) + else: + data['move'] += 1 + data['field'][x][y] = True + data['availible_moves'].remove((x, y)) + await state.update_data(data) + + keyboard = await get_keyboard(data['field']) + + content = await get_content( + data['bot_wins'], + data['user_wins'], + data['move'], + ) + + await bot.edit_message_text( + **content.as_kwargs(), + chat_id=callback.from_user.id, + message_id=callback.message.message_id, + reply_markup=keyboard, + ) + winner = await check_winner(data['field']) + if winner is True: + await callback.answer() + await start_tic_tac_toy( + callback.message, state, data['user_wins'] + 1, data['bot_wins'] + ) + return + + if not data['availible_moves']: + await callback.answer('A draw game') + await start_tic_tac_toy( + callback.message, state, data['user_wins'], data['bot_wins'] + ) + return + + x, y = choice(data['availible_moves']) + data['field'][x][y] = False + data['availible_moves'].remove((x, y)) + data['move'] += 1 + await state.update_data(data) + keyboard = await get_keyboard(data['field']) + content = await get_content( + data['bot_wins'], + data['user_wins'], + data['move'], + ) + await bot.edit_message_text( + **content.as_kwargs(), + chat_id=callback.from_user.id, + message_id=callback.message.message_id, + reply_markup=keyboard, + ) + + winner = await check_winner(data['field']) + if winner is False: + await callback.answer(text='Bot is WINNER') + await start_tic_tac_toy( + callback.message, state, data['user_wins'], data['bot_wins'] + 1 + ) + return + + await callback.answer()