Как пасьянс стал любимой игрой миллионов
Первый карточный пасьянс придумали почти 200 лет назад. Вероятно, игра имеет французское происхождение (по легенде, Наполеон Бонапарт в изгнании развлекался пасьянсом), однако наибольшее распространение она получила сначала в Великобритании, а затем в США и Канаде. Англоязычное название самой известной версии пасьянса, Klondike, возможно, связано с ее популярностью среди золотоискателей во время лихорадки в Клондайке. На русском эту версию обычно называют «Косынкой» из-за характерного расклада карт в виде треугольника.
Первый программный пасьянс, FreeCell, был разработан в конце 60-х: десятилетний Пол Альфилл успешно решил главные проблемы традиционной игры – постоянное тасование карт и подсчет очков. К 1979 году Альфилл, в то время студент медицинского факультета, написал программу для сети Университета Иллинойса PLATO, которая позволяла одновременно играть до 1000 пользователей.
В начале 90-х годов Microsoft решила включить три разновидности пасьянса в Windows, чтобы облегчить пользователям освоение новой системы и передового по тем временам усторойства – мыши. Игра стала излюбленным развлечением офисных служащих: работники настолько ею увлекались, что руководство многих крупных компаний (включая Coca-Cola, Sears и Boeing) приняло решение удалить все стандартные игры из ОС. Появлялись исследования, в которых утверждалось, что Windows-игры снижают продуктивность служащих и тем самым наносят огромный ущерб экономике – до $800 трлн в год! Пасьянс послужил причиной многочисленных увольнений, служебных разбирательств и открытия первой клиники для лечения от компьютерной зависимости. Словом, это культурный феномен, заслуживающий очередного воплощения – на самом популярном ЯП современности.
Обзор проекта

Логика игры аналогична стандартному пасьянсу 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 колонок, и визуализирует их в виде стопки рубашкой вверх.
Перемещение карт
Обработкой перемещения карт занимается метод 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
- Особенности, сферы применения, установка, онлайн IDE
- Все, что нужно для изучения Python с нуля – книги, сайты, каналы и курсы
- Типы данных: преобразование и базовые операции
- Методы работы со строками
- Методы работы со списками и списковыми включениями
- Методы работы со словарями и генераторами словарей
- Методы работы с кортежами
- Методы работы со множествами
- Особенности цикла for
- Условный цикл while
- Функции с позиционными и именованными аргументами
- Анонимные функции
- Рекурсивные функции
- Функции высшего порядка, замыкания и декораторы
- Методы работы с файлами и файловой системой
- Регулярные выражения
- Основы скрапинга и парсинга
- Основы ООП: инкапсуляция и наследование
- Основы ООП – абстракция и полиморфизм
- Графический интерфейс на Tkinter
- Основы разработки игр на Pygame
- Основы работы с SQLite
- Основы веб-разработки на Flask
- Основы работы с NumPy
- Основы анализа данных с Pandas
Комментарии