Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

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

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

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

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

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

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

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

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

Реализация

Для разработки игр на 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

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

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

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

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

В 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

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

Как написать пасьянс на Python: внутри код, логика, описание каждого шага (сохраняем)

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

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

  • draw_card_slots создает пустые слоты – для тузов, карт в 7 колонках и остатка колоды в верхнем левом углу.
  • draw_up_cards и draw_down_cards – визуализируют открытые и перевернутые карты в 7 колонках.
  • draw_remaining_cards – определяет, какие карты остались после формирования 7 колонок, и визуализирует их в виде стопки рубашкой вверх.

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

Обработкой перемещения карт занимается метод 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 помогает легко конвертировать изображения карт и иконок в удобный для отображения формат.

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

Еще больше полезной информации в канале по геймдеву:

11
1
3 комментария

о, проглиба тут акк есть, не знал

1

Стараемся)

1

Неплохо.
А теперь запусти питон на пасьянсе