Руководство по созданию игры "Найди пары" с использованием Flet#
В этом руководстве мы покажем, как шаг за шагом создать игру "Найди пары" на Python с использованием фреймворка Flet. Игра представляет собой консольное приложение из 172 строк кода (с форматированием), которое работает на десктопе, мобильных устройствах и в вебе, предоставляя интерактивный и отзывчивый интерфейс.
Зачем создавать игру "Найди пары"?#
Игра "Найди пары" идеально подходит для изучения основ Flet, так как она охватывает ключевые аспекты разработки приложений: создание макета страницы, добавление элементов управления, обработка событий, работа со списками и публикация приложения для разных платформ.
Содержание руководства#
- Начало работы с Flet
 - Создание игрового поля
 - Добавление логики игры
 - Добавление кнопки "Начать игру"
 - Финальные штрихи
 - Публикация приложения
 
1. Начало работы с Flet#
Для создания мультиплатформенного приложения с Flet не требуется знание HTML, CSS или JavaScript, но необходимы базовые знания Python и объектно-ориентированного программирования.
Установка окружения#
Убедитесь, что у вас установлен Python 3.9 или выше, а также пакет flet. Установите его с помощью команды:
pip install flet
Создайте файл hello.py для проверки работы Flet:
import flet as ft
def main(page: ft.Page):
    page.add(ft.Text(value="Привет, мир!"))
ft.app(main)
Запустите файл, и вы увидите окно с текстом "Привет, мир!".
2. Создание игрового поля#
Теперь создадим основу игры "Найди пары". Игровое поле будет состоять из карточек, которые нужно переворачивать, чтобы найти пары одинаковых изображений.
Создайте файл memo_game.py с базовой структурой:
import flet as ft
import random
def main(page: ft.Page):
    page.title = "Игра Найди пары"
    page.window_height = 710
    page.window_width = 910
    number_of_single_cards = 20
    idx_of_cards = [f"{i}_A" for i in range(number_of_single_cards)] + \
                   [f"{i}_B" for i in range(number_of_single_cards)]
    random.shuffle(idx_of_cards)
    row_board = ft.Row(wrap=True, controls=[
        ft.Container(
            content=None,
            width=100,
            height=100,
            bgcolor=ft.Colors.AMBER,
            border_radius=ft.border_radius.all(10),
        ) for _ in range(len(idx_of_cards))
    ])
    page.add(row_board)
ft.app(main)
Этот код создаёт окно с заголовком "Игра Найди пары" и сеткой из 40 карточек (20 пар), каждая из которых пока отображается как жёлтый прямоугольник.
3. Добавление логики игры#
Теперь добавим интерактивность: карточки будут открываться при клике, и мы будем проверять, совпадают ли они. Если пара найдена, карточки исчезают; если нет — они переворачиваются обратно.
Обновите memo_game.py, добавив функцию для создания карточек и обработчик кликов:
import flet as ft
import random
import time
def main(page: ft.Page):
    page.title = "Игра Найди пары"
    page.window_height = 710
    page.window_width = 910
    number_of_single_cards = 20
    image_page = "https://picsum.photos/id/"
    def make_cards(number_of_single_cards):
        card_deck_with_duplicates = \
            [f"{i}_A" for i in range(number_of_single_cards)] + \
            [f"{i}_B" for i in range(number_of_single_cards)]
        random.shuffle(card_deck_with_duplicates)
        return card_deck_with_duplicates
    idx_of_cards = make_cards(number_of_single_cards)
    list_of_pairs = []
    find = []
    def match_pairs(e):
        if len(list_of_pairs) == 2:
            return
        num = e.control.data
        idx = idx_of_cards[num].split('_')[0]
        index_of_image = idx_of_cards[num].split("_")[0]
        if len(list_of_pairs) < 2:
            e.control.content = ft.Image(src=f"{image_page+'1'+index_of_image}/100",
                                        border_radius=ft.border_radius.all(10))
            if idx_of_cards[num] not in list_of_pairs:
                list_of_pairs.append(idx_of_cards[num])
            page.update()
        if len(list_of_pairs) == 2:
            a = int(list_of_pairs[0].split('_')[0])
            b = int(list_of_pairs[1].split('_')[0])
            if a == b:
                time.sleep(0.4)
                index_a = idx_of_cards.index(str(idx)+'_A')
                index_b = idx_of_cards.index(str(idx)+'_B')
                for index in (index_a, index_b):
                    row_board.controls[index].disabled = True
                    row_board.controls[index].content = None
                    row_board.controls[index].bgcolor = ft.Colors.BLACK
                find.append(index_a)
                page.update()
                list_of_pairs.clear()
            else:
                time.sleep(1)
                flip_a = idx_of_cards.index(list_of_pairs[0])
                flip_b = idx_of_cards.index(list_of_pairs[1])
                for index in (flip_a, flip_b):
                    row_board.controls[index].content = None
                    row_board.controls[index].bgcolor = ft.Colors.AMBER
                list_of_pairs.clear()
                page.update()
    def board(idx_of_cards):
        card_images = []
        for num in range(len(idx_of_cards)):
            index_of_image = idx_of_cards[num].split("_")[0]
            card_images.append(
                ft.Container(
                    content=None,
                    width=100,
                    height=100,
                    bgcolor=ft.Colors.AMBER,
                    border_radius=ft.border_radius.all(10),
                    data=num,
                    on_click=match_pairs
                )
            )
        return card_images
    row_board = ft.Row(wrap=True, controls=board(idx_of_cards))
    page.add(row_board)
ft.app(main)
Что мы сделали:#
- Создали функцию 
make_cardsдля генерации и перемешивания карточек. - Добавили обработчик 
match_pairs, который открывает карточки, проверяет совпадения и либо убирает пары, либо переворачивает карточки обратно. - Использовали изображения с 
picsum.photosдля визуализации карточек. 
4. Добавление кнопки "Начать игру"#
Добавим кнопку, чтобы начать игру, и отобразим время, за которое игрок нашёл все пары.
Обновите memo_game.py, добавив кнопку и отслеживание времени:
import flet as ft
import random
import time
def main(page: ft.Page):
    page.title = "Игра Найди пары"
    page.window_height = 710
    page.window_width = 910
    number_of_single_cards = 20
    image_page = "https://picsum.photos/id/"
    def make_cards(number_of_single_cards):
        card_deck_with_duplicates = \
            [f"{i}_A" for i in range(number_of_single_cards)] + \
            [f"{i}_B" for i in range(number_of_single_cards)]
        random.shuffle(card_deck_with_duplicates)
        return card_deck_with_duplicates
    idx_of_cards = make_cards(number_of_single_cards)
    list_of_pairs = []
    find = []
    def start_game(e):
        e.control.disabled = True
        global start_time
        start_time = time.time()
        for i in range(len(idx_of_cards)):
            row_board.controls[i].content = None
            row_board.controls[i].bgcolor = ft.Colors.AMBER
            row_board.controls[i].border_radius = ft.border_radius.all(10)
        page.update()
    def match_pairs(e):
        if len(list_of_pairs) == 2:
            return
        if start_button.disabled:
            num = e.control.data
            idx = idx_of_cards[num].split('_')[0]
            index_a = idx_of_cards.index(str(idx)+'_A')
            index_b = idx_of_cards.index(str(idx)+'_B')
            index_of_image = idx_of_cards[num].split("_")[0]
            if len(list_of_pairs) < 2:
                e.control.content = ft.Image(src=f"{image_page+'1'+index_of_image}/100",
                                            border_radius=ft.border_radius.all(10))
                if idx_of_cards[num] not in list_of_pairs:
                    list_of_pairs.append(idx_of_cards[num])
                page.update()
            if len(list_of_pairs) == 2:
                a = int(list_of_pairs[0].split('_')[0])
                b = int(list_of_pairs[1].split('_')[0])
                if a == b:
                    time.sleep(0.4)
                    for index in (index_a, index_b):
                        row_board.controls[index].disabled = True
                        row_board.controls[index].content = None
                        row_board.controls[index].bgcolor = ft.Colors.BLACK
                    find.append(index_a)
                    if len(find) == number_of_single_cards:
                        stop_time = time.time()
                        start_button.text = f"Время: \n{round(stop_time - start_time, 0)} \nсекунд"
                        page.update()
                    page.update()
                    list_of_pairs.clear()
                else:
                    time.sleep(1)
                    flip_a = idx_of_cards.index(list_of_pairs[0])
                    flip_b = idx_of_cards.index(list_of_pairs[1])
                    for index in (flip_a, flip_b):
                        row_board.controls[index].content = None
                        row_board.controls[index].bgcolor = ft.Colors.AMBER
                    list_of_pairs.clear()
                    page.update()
    def board(idx_of_cards):
        card_images = []
        for num in range(len(idx_of_cards)):
            index_of_image = idx_of_cards[num].split("_")[0]
            card_images.append(
                ft.Container(
                    content=None,
                    width=100,
                    height=100,
                    bgcolor=ft.Colors.AMBER,
                    border_radius=ft.border_radius.all(10),
                    data=num,
                    on_click=match_pairs
                )
            )
        return card_images
    row_board = ft.Row(wrap=True, controls=board(idx_of_cards))
    start_button = ft.ElevatedButton(text="Начать игру", width=100, height=100, on_click=start_game)
    page.add(row_board, start_button)
ft.app(main)
Что мы сделали:#
- Добавили кнопку 
start_button, которая запускает игру и скрывает изображения на карточках. - Отслеживаем время начала игры (
start_time) и выводим результат, когда все пары найдены. 
5. Финальные штрихи#
Добавим заголовок и улучшим внешний вид приложения. Обновите memo_game.py:
import flet as ft
import random
import time
def main(page: ft.Page):
    page.title = "Игра Найди пары"
    page.window_height = 710
    page.window_width = 910
    page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
    page.vertical_alignment = ft.MainAxisAlignment.CENTER
    number_of_single_cards = 20
    image_page = "https://picsum.photos/id/"
    def make_cards(number_of_single_cards):
        card_deck_with_duplicates = \
            [f"{i}_A" for i in range(number_of_single_cards)] + \
            [f"{i}_B" for i in range(number_of_single_cards)]
        random.shuffle(card_deck_with_duplicates)
        return card_deck_with_duplicates
    idx_of_cards = make_cards(number_of_single_cards)
    list_of_pairs = []
    find = []
    def start_game(e):
        e.control.disabled = True
        global start_time
        start_time = time.time()
        for i in range(len(idx_of_cards)):
            row_board.controls[i].content = None
            row_board.controls[i].bgcolor = ft.Colors.AMBER
            row_board.controls[i].border_radius = ft.border_radius.all(10)
            row_board.controls[i].disabled = False
        page.update()
    def match_pairs(e):
        if len(list_of_pairs) == 2 or not start_button.disabled:
            return
        num = e.control.data
        idx = idx_of_cards[num].split('_')[0]
        index_a = idx_of_cards.index(f"{idx}_A")
        index_b = idx_of_cards.index(f"{idx}_B")
        index_of_image = idx_of_cards[num].split("_")[0]
        if len(list_of_pairs) < 2:
            e.control.content = ft.Image(src=f"{image_page+'1'+index_of_image}/100",
                                        border_radius=ft.border_radius.all(10))
            if idx_of_cards[num] not in list_of_pairs:
                list_of_pairs.append(idx_of_cards[num])
            page.update()
        if len(list_of_pairs) == 2:
            a = int(list_of_pairs[0].split('_')[0])
            b = int(list_of_pairs[1].split('_')[0])
            if a == b:
                time.sleep(0.4)
                for index in (index_a, index_b):
                    row_board.controls[index].disabled = True
                    row_board.controls[index].content = None
                    row_board.controls[index].bgcolor = ft.Colors.BLACK
                find.append(index_a)
                if len(find) == number_of_single_cards:
                    stop_time = time.time()
                    start_button.text = f"Время: \n{round(stop_time - start_time, 0)} \nсекунд"
                    page.update()
                page.update()
                list_of_pairs.clear()
            else:
                time.sleep(1)
                flip_a = idx_of_cards.index(list_of_pairs[0])
                flip_b = idx_of_cards.index(list_of_pairs[1])
                for index in (flip_a, flip_b):
                    row_board.controls[index].content = None
                    row_board.controls[index].bgcolor = ft.Colors.AMBER
                list_of_pairs.clear()
                page.update()
    def board(idx_of_cards):
        card_images = []
        for num in range(len(idx_of_cards)):
            index_of_image = idx_of_cards[num].split("_")[0]
            card_images.append(
                ft.Container(
                    content=None,
                    width=100,
                    height=100,
                    bgcolor=ft.Colors.AMBER,
                    border_radius=ft.border_radius.all(10),
                    data=num,
                    on_click=match_pairs
                )
            )
        return card_images
    row_board = ft.Row(wrap=True, controls=board(idx_of_cards))
    start_button = ft.ElevatedButton(text="Начать игру", width=100, height=100, on_click=start_game)
    page.add(
        ft.Column(
            alignment=ft.MainAxisAlignment.CENTER,
            controls=[
                ft.Text(value="Найди пары", theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM),
                row_board,
                start_button
            ]
        )
    )
ft.app(main)
Что мы сделали:#
- Добавили заголовок "Найди пары" в центре страницы.
 - Выровняли содержимое по центру с помощью 
page.horizontal_alignmentиpage.vertical_alignment. - Упаковали элементы в 
Columnдля лучшей организации интерфейса. 
6. Публикация приложения#
Поздравляем! Вы создали игру "Найди пары" с использованием Flet. Теперь вы можете опубликовать её как десктопное, мобильное или веб-приложение.
Для публикации используйте команды Flet:
- Веб-приложение: 
flet run --web - Десктопное приложение: Упакуйте приложение с помощью 
flet build(требуется, чтобы проект был по структуре, как в пункте Подготовка дирректории) 
Итог#
В этом руководстве вы научились:
- Создавать простое приложение с Flet.
 - Работать с макетами (
Row,Column). - Обрабатывать события (клики по карточкам и кнопке).
 - Реализовывать игровую логику (переворот карточек, проверка пар).
 - Публиковать приложение для разных платформ.
 
Самостяотельная работа#
- Попробуйте сделать так, чтобы данное приложение считыало ваши картинки из папки 
assets - Поробуйте сделать рабочий билд программы (
flet build)