Почему aiogram выдаёт KeyError при обработке callback_query из личных сообщений?

вылезает ошибка

Cause exception while process update id=691239273 by bot id=8510264806
KeyError: 7926248376
Traceback (most recent call last):
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\dispatcher.py", line 309, in _process_update
    response = await self.feed_update(bot, update, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\dispatcher.py", line 158, in feed_update
    response = await self.update.wrap_outer_middleware(
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    ...<7 lines>...
    )
    ^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\middlewares\error.py", line 25, in __call__
    return await handler(event, data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\middlewares\user_context.py", line 56, in __call__
    return await handler(event, data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\fsm\middleware.py", line 42, in __call__
    return await handler(event, data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\event\telegram.py", line 121, in trigger
    return await wrapped_inner(event, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\event\handler.py", line 43, in call
    return await wrapped()
           ^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\dispatcher.py", line 276, in _listen_update
    return await self.propagate_event(update_type=update_type, event=event, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\router.py", line 146, in propagate_event
    return await observer.wrap_outer_middleware(_wrapped, event=event, data=kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\router.py", line 141, in _wrapped
    return await self._propagate_event(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        observer=observer, update_type=update_type, event=telegram_event, **data
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\router.py", line 166, in _propagate_event
    response = await observer.trigger(event, **kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\event\telegram.py", line 121, in trigger
    return await wrapped_inner(event, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\.venv\Lib\site-packages\aiogram\dispatcher\event\handler.py", line 43, in call
    return await wrapped()
           ^^^^^^^^^^^^^^^
  File "C:\Users\alanm\PycharmProjects\Uno\main.py", line 174, in play
    g = games[cb.message.chat.id]
        ~~~~~^^^^^^^^^^^^^^^^^^^^
KeyError: 7926248376
import random
import asyncio
from aiogram import Bot, Dispatcher
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, CallbackQuery
from aiogram.filters import Command

TOKEN = "ТОКЕН"
bot = Bot(token=TOKEN)
dp = Dispatcher()

games = {}

COLOR_EMOJI = {
    "Синяя": "?",
    "Красная": "?",
    "Желтая": "?",
    "Зеленая": "?",
}


class UnoGame:
    def __init__(self, chat_id):
        self.chat_id = chat_id
        self.players = []
        self.settings = {
            "cards_per_player": 7,
            "allow_plus2": True,
            "allow_skip": True,
            "allow_reverse": True,
            "allow_plus4": True,
            "max_players": 4
        }

        self.started = False
        self.deck = self.generate_deck()
        self.hands = {}
        self.current_card = None
        self.current_color = None
        self.turn = 0
        self.direction = 1


    def generate_deck(self):
        colors = ["Синяя", "Красная", "Желтая", "Зеленая"]
        values = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
        deck = []

        for color in colors:
            for v in values:
                deck.append(f"{color} {v}")
            deck.append(f"{color} +2")
            deck.append(f"{color} Пропуск")
            deck.append(f"{color} Реверс")

        for _ in range(4): deck.append("+4")
        for _ in range(4): deck.append("Смена цвета")

        random.shuffle(deck)
        return deck


    def deal(self):
        for p in self.players:
            self.hands[p] = [self.deck.pop() for _ in range(self.settings["cards_per_player"])]
        self.current_card = self.deck.pop()

        if self.current_card in ["+4", "Смена цвета"]:
            self.current_color = random.choice(["Синяя", "Красная", "Желтая", "Зеленая"])
        else:
            self.current_color = self.current_card.split()[0]


    def is_valid(self, card):
        if card == "+4" or card == "Смена цвета":
            return True

        color, value = card.split()[0], " ".join(card.split()[1:])
        top_val = " ".join(self.current_card.split()[1:]) if self.current_card not in ["+4", "Смена цвета"] else None

        return color == self.current_color or value == top_val


    def next_turn(self):
        self.turn = (self.turn + self.direction) % len(self.players)


def settings_keyboard(chat_id):
    g = games[chat_id]
    return InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text=f"Карт: {g.settings['cards_per_player']}", callback_data="set_cards")],
        [InlineKeyboardButton(text=f"+2: {'Вкл' if g.settings['allow_plus2'] else 'Выкл'}", callback_data="set_plus2")],
        [InlineKeyboardButton(text=f"Пропуск: {'Вкл' if g.settings['allow_skip'] else 'Выкл'}", callback_data="set_skip")],
        [InlineKeyboardButton(text=f"Реверс: {'Вкл' if g.settings['allow_reverse'] else 'Выкл'}", callback_data="set_rev")],
        [InlineKeyboardButton(text=f"+4: {'Вкл' if g.settings['allow_plus4'] else 'Выкл'}", callback_data="set_plus4")],
        [InlineKeyboardButton(text=f"Макс игроков: {g.settings['max_players']}", callback_data="set_max")],
        [InlineKeyboardButton(text="▶ Начать игру", callback_data="start_uno")]
    ])


def hand_keyboard(pid, chat_id):
    g = games[chat_id]
    kb = []
    for card in g.hands[pid]:
        kb.append([InlineKeyboardButton(text=card, callback_data=f"play_{card}")])
    kb.append([InlineKeyboardButton(text="Взять карту", callback_data="take")])
    return InlineKeyboardMarkup(inline_keyboard=kb)


def choose_color_keyboard():
    return InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="? Синяя", callback_data="color_Синяя")],
        [InlineKeyboardButton(text="? Красная", callback_data="color_Красная")],
        [InlineKeyboardButton(text="? Желтая", callback_data="color_Желтая")],
        [InlineKeyboardButton(text="? Зеленая", callback_data="color_Зеленая")]
    ])


@dp.message(Command("game"))
async def game_cmd(msg: Message):
    if msg.chat.id not in games:
        games[msg.chat.id] = UnoGame(msg.chat.id)
    await msg.answer("UNO — набор игроков!", reply_markup=InlineKeyboardMarkup(inline_keyboard=[[InlineKeyboardButton(text="Присоединиться", callback_data="join")]]))

@dp.callback_query(lambda c: c.data == "join")
async def join(cb: CallbackQuery):
    g = games[cb.message.chat.id]

    if cb.from_user.id in g.players:
        return await cb.answer("Ты уже в игре!")

    if len(g.players) >= g.settings["max_players"]:
        return await cb.answer("Лобби заполнено!")

    g.players.append(cb.from_user.id)
    await cb.message.answer(f"Игрок {cb.from_user.first_name} вошёл в игру!")

    if len(g.players) >= 2:
        await cb.message.answer("Настройки:", reply_markup=settings_keyboard(cb.message.chat.id))


@dp.callback_query(lambda c: c.data.startswith("set_"))
async def set_update(cb: CallbackQuery):
    g = games[cb.message.chat.id]

    if cb.data == "set_cards":
        g.settings["cards_per_player"] = (g.settings["cards_per_player"] % 12) + 1
    if cb.data == "set_plus2": g.settings["allow_plus2"] ^= True
    if cb.data == "set_skip": g.settings["allow_skip"] ^= True
    if cb.data == "set_rev": g.settings["allow_reverse"] ^= True
    if cb.data == "set_plus4": g.settings["allow_plus4"] ^= True
    if cb.data == "set_max":
        g.settings["max_players"] = (g.settings["max_players"] % 8) + 2

    await cb.message.edit_reply_markup(reply_markup=settings_keyboard(cb.message.chat.id))
    await cb.answer()


@dp.callback_query(lambda c: c.data == "start_uno")
async def start_game(cb: CallbackQuery):
    g = games[cb.message.chat.id]
    g.started = True
    g.deal()
    await cb.message.answer(f"Игра началась! Верхняя карта: {g.current_card}")

    cur = g.players[g.turn]
    await bot.send_message(cur, f"Твой ход! Цвет: {g.current_color}", reply_markup=hand_keyboard(cur, g.chat_id))


@dp.callback_query(lambda c: c.data.startswith("play_"))
async def play(cb: CallbackQuery):
    g = games[cb.message.chat.id]
    pid = cb.from_user.id

    if pid != g.players[g.turn]: return await cb.answer("Не твой ход!")

    card = cb.data.replace("play_", "")

    if not g.is_valid(card):
        return await cb.answer("Карта не подходит!")

    g.hands[pid].remove(card)
    g.current_card = card

    if card == "+4" or card == "Смена цвета":
        await cb.message.edit_text(f"Ты сыграл {card}. Выбери цвет:")
        return await cb.message.edit_reply_markup(reply_markup=choose_color_keyboard())

    color = card.split()[0]
    g.current_color = color

    effects = " ".join(card.split()[1:])

    if effects == "+2" and g.settings["allow_plus2"]:
        target = g.players[(g.turn + g.direction) % len(g.players)]
        g.hands[target].extend([g.deck.pop(), g.deck.pop()])

    if effects == "Пропуск" and g.settings["allow_skip"]:
        g.next_turn()

    if effects == "Реверс" and g.settings["allow_reverse"]:
        g.direction *= -1

    if len(g.hands[pid]) == 0:
        await bot.send_message(g.chat_id, f"? Победил {cb.from_user.first_name}!")
        games.pop(g.chat_id)
        return

    g.next_turn()
    nxt = g.players[g.turn]
    await cb.message.edit_text(f"Ты сыграл {card}")
    await bot.send_message(nxt, f"Твой ход! Цвет: {g.current_color}", reply_markup=hand_keyboard(nxt, g.chat_id))


@dp.callback_query(lambda c: c.data.startswith("color_"))
async def set_color(cb: CallbackQuery):
    g = games[cb.message.chat.id]
    new_color = cb.data.split("_")[1]
    g.current_color = new_color

    await cb.message.edit_text(f"Цвет выбран: {new_color}")

    g.next_turn()
    nxt = g.players[g.turn]
    await bot.send_message(nxt, f"Твой ход! Цвет: {g.current_color}", reply_markup=hand_keyboard(nxt, g.chat_id))


@dp.callback_query(lambda c: c.data == "take")
async def take(cb: CallbackQuery):
    g = games[cb.message.chat.id]
    pid = cb.from_user.id

    if pid != g.players[g.turn]:
        return await cb.answer("Сейчас не твой ход!")

    if not g.deck:
        g.deck = g.generate_deck()
        random.shuffle(g.deck)

    card = g.deck.pop()
    g.hands[pid].append(card)

    await cb.message.edit_text(f"Ты взял карту: {card}")
    await cb.answer()

    await bot.send_message(pid, "Твоя рука обновлена:", reply_markup=hand_keyboard(pid, g.chat_id))

    g.next_turn()
    nxt = g.players[g.turn]
    await bot.send_message(nxt, f"Твой ход! Цвет: {g.current_color}", reply_markup=hand_keyboard(nxt, g.chat_id))


def collect_all_cards(game: UnoGame):
    all_cards = []
    full = game.generate_deck()
    used = []
    for hand in game.hands.values():
        used.extend(hand)
    if game.current_card:
        used.append(game.current_card)
    new_deck = [c for c in full if c not in used]
    random.shuffle(new_deck)
    return new_deck

@dp.callback_query(lambda c: c.data.startswith("color_"))
async def set_color(cb: CallbackQuery):
    g = games[cb.message.chat.id]
    new_color = cb.data.split("_", 1)[1]
    prev_card = g.current_card

    g.current_color = new_color
    await cb.message.edit_text(f"Цвет выбран: {new_color}")
    await cb.answer(f"Выбран цвет: {new_color}")

    if prev_card == "+4" and g.settings.get("allow_plus4", True):
        target_index = (g.turn + g.direction) % len(g.players)
        target = g.players[target_index]
        while len(g.deck) < 4:
            g.deck.extend(collect_all_cards(g))
            if not g.deck:
                g.deck = g.generate_deck()
            random.shuffle(g.deck)

        drawn = [g.deck.pop() for _ in range(4)]
        g.hands[target].extend(drawn)
        try:
            await bot.send_message(target, f"Тебе дали +4 карты: {', '.join(drawn)}")
        except Exception:
            pass

    g.next_turn()
    nxt = g.players[g.turn]
    await bot.send_message(nxt, f"Твой ход! Цвет: {g.current_color}", reply_markup=hand_keyboard(nxt, g.chat_id))

def ensure_deck_has(game: UnoGame, need: int):
    if len(game.deck) >= need:
        return
    new_cards = collect_all_cards(game)
    game.deck.extend(new_cards)
    random.shuffle(game.deck)

@dp.message(Command("help"))
async def help_cmd(message: Message):
    help_text = (
        "<b>UNO — Помощь и правила</b>\n\n"
        "Это Telegram-бот для игры в UNO. Кратко и подробно о возможностях и командах:\n\n"
        "<b>Команды</b>:\n"
        "/game — создать лобби и начать набор игроков.\n"
        "/help — показать эту справку.\n\n"
        "<b>Как присоединиться</b>:\n"
        "1. В чате группы вызовите /game.\n"
        "2. Нажмите кнопку «Присоединиться» в лобби.\n"
        "3. Как только в лобби будет ≥2 игрока — появятся настройки игры.\n\n"
        "<b>Настройки (в лобби)</b>:\n"
        "• <i>Карт</i> — количество карт, выдаваемых каждому игроку (по умолчанию 7).\n"
        "• <i>+2</i>, <i>Пропуск</i>, <i>Реверс</i>, <i>+4</i> — включение/выключение соответствующих эффектов.\n"
        "• <i>Макс игроков</i> — ограничение размера лобби.\n"
        "• Нажмите ▶ «Начать игру», чтобы раздать карты и начать.\n\n"
        "<b>Игровой процесс</b>:\n"
        "• Игроку на телефон/личный чат присылается клавиатура — его карты (inline-кнопки).\n"
        "• Нажмите кнопку с картой, чтобы сыграть её. Бот проверит корректность хода.\n"
        "• Кнопка «Взять карту» — взять одну карту из колоды (ход переходит дальше).\n"
        "• После хода бот передаст ход следующему игроку и пришлёт ему клавиатуру с картами.\n\n"
        "<b>Специальные карты</b>:\n"
        "• <b>+2</b> — следующий игрок берёт 2 карты (если включён в настройках).\n"
        "• <b>Пропуск</b> — следующий игрок пропускает ход.\n"
        "• <b>Реверс</b> — меняет направление очередности.\n"
        "• <b>Смена цвета</b> — джокер: игрок выбирает новый цвет через кнопки.\n"
        "• <b>+4</b> — джокер со штрафом: игрок выбирает цвет, а следующий игрок берёт 4 карты (если включён).\n\n"
        "<b>Поведение джокеров</b>:\n"
        "• Когда вы сыграли «Смена цвета» или «+4», бот пришлёт кнопки выбора цвета (????).\n"
        "• После выбора цвета игра продолжается с новым верхним цветом.\n\n"
        "<b>Победа</b>:\n"
        "• Игрок, который избавился от всех карт — победитель. Бот объявит победителя в чате.\n\n"
        "<b>Колода и восстановление</b>:\n"
        "• Если колода пустеет, бот автоматически перемешает использованные карты и восстановит колоду.\n\n"
        "Удачи и хорошей игры! ?"
    )

    kb = InlineKeyboardMarkup(inline_keyboard=[
        [InlineKeyboardButton(text="▶ Начать игру", callback_data="start_uno")]
    ])

    await message.answer(help_text, reply_markup=kb, parse_mode="HTML")



async def main():
    await dp.start_polling(bot)


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except (KeyboardInterrupt, SystemExit):
        print("Bot stopped")

в чем проблема подскажите?


Ответы (0 шт):