Перейти к содержанию

Руководство по созданию игры "Найди пары" с использованием Flet#

В этом руководстве мы покажем, как шаг за шагом создать игру "Найди пары" на Python с использованием фреймворка Flet. Игра представляет собой консольное приложение из 172 строк кода (с форматированием), которое работает на десктопе, мобильных устройствах и в вебе, предоставляя интерактивный и отзывчивый интерфейс.

Зачем создавать игру "Найди пары"?#

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

Содержание руководства#

  1. Начало работы с Flet
  2. Создание игрового поля
  3. Добавление логики игры
  4. Добавление кнопки "Начать игру"
  5. Финальные штрихи
  6. Публикация приложения

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)