🐍🃏 Как написать пасьянс на Python

Пасьянс «Косынка» – одна из самых популярных карточных игр. В этой статье мы разберем, как реализовать «Косынку» с использованием библиотеки Tkinter: детально рассмотрим логику игры, настройки интерфейса, а также визуализацию и обработку перемещений карт.

Как пасьянс стал любимой игрой миллионов

Первый карточный пасьянс придумали почти 200 лет назад. Вероятно, игра имеет французское происхождение (по легенде, Наполеон Бонапарт в изгнании развлекался пасьянсом), однако наибольшее распространение она получила сначала в Великобритании, а затем в США и Канаде. Англоязычное название самой известной версии пасьянса, Klondike, возможно, связано с ее популярностью среди золотоискателей во время лихорадки в Клондайке. На русском эту версию обычно называют «Косынкой» из-за характерного расклада карт в виде треугольника.

Первый программный пасьянс, FreeCell, был разработан в конце 60-х: десятилетний Пол Альфилл успешно решил главные проблемы традиционной игры – постоянное тасование карт и подсчет очков. К 1979 году Альфилл, в то время студент медицинского факультета, написал программу для сети Университета Иллинойса PLATO, которая позволяла одновременно играть до 1000 пользователей.

В начале 90-х годов Microsoft решила включить три разновидности пасьянса в Windows, чтобы облегчить пользователям освоение новой системы и передового по тем временам усторойства – мыши. Игра стала излюбленным развлечением офисных служащих: работники настолько ею увлекались, что руководство многих крупных компаний (включая Coca-Cola, Sears и Boeing) приняло решение удалить все стандартные игры из ОС. Появлялись исследования, в которых утверждалось, что Windows-игры снижают продуктивность служащих и тем самым наносят огромный ущерб экономике – до $800 трлн в год! Пасьянс послужил причиной многочисленных увольнений, служебных разбирательств и открытия первой клиники для лечения от компьютерной зависимости. Словом, это культурный феномен, заслуживающий очередного воплощения – на самом популярном ЯП современности.

Обзор проекта

Пасьянс «Косынка» на Python

Логика игры аналогична стандартному пасьянсу Windows:

  • Колода состоит из 52 карт.
  • Основная цель – собрать все карты в четыре стопки (по мастям) в порядке возрастания от туза до короля.
  • Семь столбцов карт выкладываются слева направо. Первый столбец состоит из одной открытой карты, второй – из двух карт (одной закрытой и одной открытой сверху), третий – из трех карт (две закрытых и одна открытая сверху) и так далее до седьмого столбца, где будет семь карт (шесть закрытых и одна открытая).
  • Оставшиеся карты кладутся в отдельную стопку. В стандартном режиме пересдать (перебрать) карты можно 3 раза, в тренировочном режиме пересдачи не ограничены.

В игре реализована функция визуальной подсказки:

Рекомендуемый ход подсвечивается зеленым цветом

Настройки игры можно изменить на ходу, без перезапуска расклада. Опции включают:

  • Выбор способа перемещения карт – клик или перетаскивание.
  • Режим игры – стандартный или тренировочный.
  • Рубашка (обратная сторона карт) – темная или светлая.
  • Цвет фона.
  • Отображение верхнего меню и футера со статистикой.
После сохранения настроек игра продолжается с того же места, где прервалась

Готовый код, изображения карт и иконки находятся в этом репозитории.

Реализация

Для разработки игр на Python обычно используют библиотеку Pygame. Однако для создания карточных игр вполне достаточно стандартной GUI-библиотеки Tkinter: в ней есть функциональность, оптимально подходящая для обработки манипуляций с картами – find_overlapping, bbox, find_withtag, gettags и addtag_withtag:

  • Метод bbox используется для определения границ объекта на холсте. Он возвращает кортеж (x1, y1, x2, y2) с координатами ограничивающего прямоугольника:
bbox = self.canvas.bbox(tag_or_id)
  • Метод find_overlapping возвращает все объекты, которые пересекаются с указанным прямоугольником и как нельзя лучше подходит для поиска карт, находящихся в определенной области:
overlapping_items = self.canvas.find_overlapping(x1, y1, x2, y2)
  • Метод find_withtag используется для поиска элементов, которым назначен определенный тег (или идентификатор). Возвращает кортеж с уникальными ID всех элементов, соответствующих указанному тегу:
ids = canvas.find_withtag(tag)
  • Метод gettags позволяет динамически проверять и анализировать свойства объектов на холсте. В этом пасьянсе он используется для управления игровыми элементами (карты, слоты и активные объекты), определяя их статус и поведение на основе тегов – например, помогает узнать, является ли карта открытой, перевернутой или активной:
if "empty" in str(self.canvas.gettags(item)):
    continue
elif "face_down" in str(self.canvas.gettags(item)):
    continue
  • Метод canvas.addtag_withtag добавляет новый тег к объекту, который уже имеют определенный существующий тег. В игре этот метод используется для динамической модификации свойств карт:
    def change_current(self, tag):
        self.canvas.dtag("current")
        self.canvas.focus("")
        self.canvas.addtag_withtag("current", tag)

Метод из библиотеки Pillow, ImageTk.PhotoImage, конвертирует любые изображения в объекты, которые можно отобразить в интерфейсе Tkinter. Эта функция конвертирует изображения карт и иконки для использования в навигационной панели в качестве кнопок:

    def convert_pictures(self, url, main=True):
        picture = Image.open(os.path.join("assets", url))
        picture = (ImageTk.PhotoImage(picture))
        if main:
            self.canvas.images.append(picture)
        else:
            self.button_images.append(picture)
        return picture                
  

Верхнее меню и футер

В верхнем меню располагаются кнопки, с помощью которых можно:

  • Запустить новую игру.
  • Перезапустить текущую игру.
  • Отменить и повторить ход.
  • Показать подсказку.
  • Открыть настройки.
  • Перейти в полноэкранный режим (и выйти из него).

Если в определенный момент какая-то операция недоступна (например, отмена и повтор хода до начала игры), эта кнопка будет отображаться в неактивном виде:

При наведении на кнопку всплывают подсказки (тултипы):

В tkinter.tix есть метод для создания тултипов (balloon), но он уже устарел – при его использовании вы получите предупреждение о скором прекращении поддержки. Поэтому в пасьянсе используются кастомные методы для показа/скрытия тултипов:

    def show_tooltip(self, event):
        if not self.text:  
            return

        self.tooltip_window = tk.Toplevel(self)
        self.tooltip_window.wm_overrideredirect(True)  
        self.tooltip_window.wm_attributes("-topmost", True)  
        tooltip_label = tk.Label(self.tooltip_window, text=self.text, justify=self.ttjustify,
                                 background=self.ttbackground, foreground=self.ttforeground,
                                 relief=self.ttrelief, borderwidth=self.ttborderwidth,
                                 font=self.ttfont)
        tooltip_label.pack(ipadx=5, ipady=2)
        x = event.x_root + (10 if not self.ttlocationinvert else -10)
        y = event.y_root + (20 if not self.ttheightinvert else -20)
        self.tooltip_window.wm_geometry(f"+{x}+{y}")

    def hide_tooltip(self):
        if self.tooltip_window:
            self.tooltip_window.destroy()
            self.tooltip_window = None

Если отключить верхнее меню, иконка настроек будет расположена слева над футером:

В футере выводится статистика – число оставшихся пересдач (для стандартного режима), количество карт, очки и затраченное на игру время (таймер останавливается во время автоматического перемещения карт в слоты для тузов):

Отрисовка карт

Карты визуализируются на игровом поле с помощью этих методов:

  • draw_card_slots создает пустые слоты – для тузов, карт в 7 колонках и остатка колоды в верхнем левом углу.
  • draw_up_cards и draw_down_cards – визуализируют открытые и перевернутые карты в 7 колонках.
  • draw_remaining_cards – определяет, какие карты остались после формирования 7 колонок, и визуализирует их в виде стопки рубашкой вверх.
🐍 Библиотека питониста
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста»
🐍🎓 Библиотека Python для собеса
Подтянуть свои знания по Python вы можете на нашем телеграм-канале «Библиотека Python для собеса»
🐍🧩 Библиотека задач по Python
Интересные задачи по Python для практики можно найти на нашем телеграм-канале «Библиотека задач по Python»

Перемещение карт

Обработкой перемещения карт занимается метод move_card. Когда игрок нажимает на карту, метод проверяет, находится ли карта в состоянии перемещения self.move_flag. Если нет, то все видимые карты помечаются тегом "moveable", чтобы их можно было перемещать. Затем уровень слоя выбранной карты поднимается с помощью tag_raise("moveable"), а координаты курсора event.x и event.y записываются как начальные.

При каждом движении мыши с нажатой кнопкой пересчитывается смещение курсора. Карта перемещается на это смещение относительно своих текущих координат self.canvas.move("moveable", ...), после чего вызывается подсветка допустимых мест для перемещения. Подсветка допустимых позиций highlight_available_cards проверяет пересечения текущей перемещаемой карты с другими объектами, вычисляет bbox-область вокруг карты и ищет все объекты, которые пересекаются с этой областью. При этом метод игнорирует неподходящие объекты (например, рубашкой вниз или пустые слоты). Если найдена подходящая карта или слот, создается полупрозрачный прямоугольник для подсветки, а все карты в стопке поднимаются выше подсветки self.canvas.tag_raise(card):

    def highlight_available_cards(self):
        try:
            last_image_location = self.current_image_location
            returnval = True
            bbox = list(self.canvas.bbox(self.canvas.find_withtag("current")))
            bbox[0] = int(bbox[0]) - 15
            bbox[1] = int(bbox[1]) - 15
            bbox[2] = int(bbox[2]) + 15
            bbox[3] = int(bbox[3]) + 15
            card_overlapping = self.canvas.find_overlapping(*tuple(bbox))
        except:
            return
        for card in card_overlapping:
            if (self.canvas.find_withtag("current")) == card:
                continue
            card_tag = str(self.canvas.gettags(card))
            if "face_down" in card_tag:
                continue
            elif "current" in card_tag:
                continue
            elif "empty_cardstack_slot" in card_tag:
                break
            else:
                current_image = self.canvas.find_withtag(card)
                current_image_bbox = self.canvas.bbox(current_image)
                returnval = self.generate_returnval(current_image)
                if returnval:
                    self.create_rectangle(
                        *current_image_bbox, fill="blue", alpha=.3, tag="available_card_rect")
                    for card in self.card_stack_list:
                        if "empty_slot" not in self.canvas.gettags(card):
                            self.canvas.tag_raise(card)
                    continue

Когда игрок отпускает карту, вызывается метод drop_card. Он удаляет подсветку допустимых позиций и снимает с карты тег "moveable", после чего проверяет пересечения с другими объектами в текущей позиции карты. Если карта пересекается с допустимым местом, проверяется правильность хода (то есть можно ли положить карту на выбранное место в соответствии с установленными для этой стопки правилами). Если можно, то карта размещается сверху стопки, если нет – возвращается на исходную позицию.

Генерация подсказок

Функция generate_hint отвечает за генерацию подсказок в игре, показывая возможный ход для игрока. Каждая карта из доступных face_up_cards анализируется на возможность перемещения. Для этого:

  • Вычисляются bbox-координаты и области пересечения.
  • Проверяются правила перемещения – можно ли положить карту на другую стопку, ассоциировать с пустым слотом или слотом для туза. Кроме того, проверяется валидность хода через check_move_validity и не находится ли карта над запрещенными слотами.
  • Короли и тузы рассматриваются отдельно, так как у них особые правила (только туз может быть первой картой в слоте для туза; только короля можно положить на пустой слот в одной из 7 колонок).

Визуально пара карт для возможного хода подсвечивается зеленым светом:

if returnval:
    self.create_rectangle(*card_b_bbox, fill="green", alpha=.5)
    self.create_rectangle(*card_a_bbox, fill="green", alpha=.5)

Автоматическое перемещение в базу

Функция send_cards_up отвечает за автоматическое перемещение всех подходящих карт на соответствующие слоты для тузов (то есть в базу/дом). Чтобы обработать все ситуации, включая освобождение новых карт для перемещения, функция использует циклы, фильтры и рекурсивный подход:

  • Собирает список всех открытых карт face_up_cards.
  • Исключает карты, перекрытые другими картами.
  • Проверяет, можно ли переложить какую-либо карту в соответствующую ей базу.
  • Рекурсивно вызывает себя, чтобы проверить, не открылись ли еще какие-нибудь подходящие для перемещения карты.

В заключение

Tkinter и Pillow предоставляют функциональность для работы с графическими объектами, которая идеально подходит для реализации пасьянса:

  • Методы bbox, find_overlapping, find_withtag, gettags и addtag_withtag упрощают обработку взаимодействий между картами и их состояниями.
  • ImageTk.PhotoImage помогает легко конвертировать изображения карт и иконок в удобный для отображения формат.

Эти методы превращают создание сложной карточной игры в интуитивный процесс: большая часть логики взаимодействия объектов реализуется буквально несколькими строками кода. Функциональность готовой игры при этом сопоставима с профессиональными версиями пасьянса.

***

Самоучитель по Python

  1. Особенности, сферы применения, установка, онлайн IDE
  2. Все, что нужно для изучения Python с нуля – книги, сайты, каналы и курсы
  3. Типы данных: преобразование и базовые операции
  4. Методы работы со строками
  5. Методы работы со списками и списковыми включениями
  6. Методы работы со словарями и генераторами словарей
  7. Методы работы с кортежами
  8. Методы работы со множествами
  9. Особенности цикла for
  10. Условный цикл while
  11. Функции с позиционными и именованными аргументами
  12. Анонимные функции
  13. Рекурсивные функции
  14. Функции высшего порядка, замыкания и декораторы
  15. Методы работы с файлами и файловой системой
  16. Регулярные выражения
  17. Основы скрапинга и парсинга
  18. Основы ООП: инкапсуляция и наследование
  19. Основы ООП – абстракция и полиморфизм
  20. Графический интерфейс на Tkinter
  21. Основы разработки игр на Pygame
  22. Основы работы с SQLite
  23. Основы веб-разработки на Flask
  24. Основы работы с NumPy
  25. Основы анализа данных с Pandas

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

admin
11 декабря 2018

ООП на Python: концепции, принципы и примеры реализации

Программирование на Python допускает различные методологии, но в его основе...
admin
13 февраля 2017

Программирование на Python: от новичка до профессионала

Пошаговая инструкция для всех, кто хочет изучить программирование на Python...