Как я поэта поздравлял

Мой брат — поэт.

Меня всегда восхищало, как человек может взять и написать такие строчки, что заиграют струны души, да ещё и в рифму!

День рождения брата, мне 10, гитара у груди (играть я совершенно не умел), его стихотворение перед глазами и я пою с улыбкой и свежестью, как пел гимн школы на первое сентября.

Исполняю стихотворение брата на его день рождения

Исполняю стихотворение брата на его день рождения

Прошло 8 лет… Я подрос, вырос и брат. И вот приближается его день рождения. Внутри меня порыв, внутри меня желание… поздравить, но как-то с изюминкой, как-то… с душой что ли.

Я заканчиваю школу, имею небольшой опыт программирования на Python, но большое желание претворять идеи в жизнь. И лампочка в голове зажигается — поэтическая викторина!

Идея

Выводится строка стихотворения, а ниже — несколько вариантов ответов (различные строки) и нужно выбрать правильный ответ (то есть — правильное продолжение стихотворения).

Если игрок допускает ошибку, то игра завершается и ему предлагается начать заново, причём стихотворение (которое нужно отгадать) выбирается случайным образом.

Создание поэтической викторины

Шаг первый — графическое представление

Начать я решил с описания схемы программы:

  • Как она должна выглядеть

  • Какие есть окна

  • Какие доступны возможности

Схематическое представление

Схематическое представление «Поэтической викторины»

Решил, что будет три окна:

  • Основное, где будут располагаться кнопки для начала игры, открытия меню стихотворений и закрытия программы

  • Меню стихотворений, в котором можно посмотреть и прочитать все доступные в игре стихотворения

  • Игровое окно, в котором выводится строка стихотворения, а также варианты ответа и возможность сменить стихотворение (если текущее не нравится)

Вторая ступенька — написание программы

Не стал мудрить и решил использовать то, что знаю — Python. Для графического интерфейса выбрал модуль tkinter.

Во избежание конфликтов из-за версий модулей решил работать в виртуальном окружении. Для размещения кода проекта выбрал GitHub.

Структура проекта:

  • venv — директория для виртуального окружения

  • .gitignore — файл для игнорирования определённых файлов и директорий при отправлении изменений через Git

  • constants.py — файл с константами

  • main.py — файл для запуска программы

  • poetry.py — файл со стихотворениями

  • README.md — файл описывающий проект

  • ui.py — файл с логикой работы UI

Структура проекта

Структура проекта

В .gitignore добавил:

 .idea
/venv/

constants.py

MAIN_BUTTON_FONT = "Arial 24"
ANSWER_BUTTON_FONT = "Arial 20"
BLACK = "black"
WHITE = "white"

main.py:

from ui import UI

if __name__ == "__main__":
    ui = UI()
    ui.start_ui()

Для стихотворений выбрал тип данных Python — словарь.
Название стихотворения является ключом словаря, а текст — значением. Текст стихотворения оформлен в виде списка. Элементы списка — строки стихотворения.

poetry.py:

poetry_dict = {
    "название_вашего_стихотворения": [
        "Первая строка стихотворения,",
        "Вторая строка стихотворения.",
    ],
    "Всегда": [
        "Всегда,",
        "Что-то происходит.",
        "Взлёты и падения,",
        "Когда-то проходят.",
        "Возможно научиться их ценить,",
        "Учит наша короткая жизнь.",
    ],
    "Dark night": [
        "Yeah, I guess I’m alright,",
        "Writing some notes,",
        "To get rid of my thoughts,",
        "The past and future all combined,",
        "Been heavy lately on my mind,",
        "I am sure it’s temporary,",
        "No need more worries,",
        "The good side actually,",
        "I will be able to tell more stories.",
    ],
}

Наибольшая часть кода расположена в файле ui.py.
Решил сделать UI через класс и использовать self и статичные методы.

Понадобились следующие импорты:

import random
import tkinter as tk
from tkinter import ttk
from constants import ANSWER_BUTTON_FONT, BLACK, MAIN_BUTTON_FONT, WHITE
from poetry import poetry_dict

Описание инициализации:

class UI:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("Poetry Quiz")
        self.root.configure(background="gray")
        self.root.geometry("900x600")

Добавляем необходимые переменные:

        self.poetry_part = tk.StringVar()
        self.poetry_titles = [element for element in poetry_dict.keys()]
        self.poetry_showing_name = ""
        self.poetry_showing = []

        self.step = 0
        self.last_step = 0
        self.mistakes = 0

Создаём UI-элементы (текст, кнопки, комбинированный список):

        self.poetry_label = tk.Label(
            self.root, textvariable=self.poetry_part, font="Arial 26"
        )
        self.start_label = tk.Label(self.root, text="Poetry Quiz", font="Arial 36")
        self.start_label.pack(fill="both", expand=True)
        self.start_button = tk.Button(
            self.root,
            text="Начать игру",
            background=WHITE,
            foreground=BLACK,
            highlightbackground="green",
            highlightthickness=2.5,
            font=MAIN_BUTTON_FONT,
            command=self.play_game,
            justify=tk.CENTER,
        )
        self.start_button.pack(expand=True)

        self.poetry_button = tk.Button(
            self.root,
            text="Посмотреть стихотворения",
            background=WHITE,
            foreground=BLACK,
            highlightbackground="orange",
            highlightthickness=2.5,
            font=MAIN_BUTTON_FONT,
            command=self.open_poetry_menu,
            justify=tk.CENTER,
        )
        self.poetry_button.pack(expand=True)

        self.exit_button = tk.Button(
            self.root,
            text="Завершить игру",
            background=WHITE,
            foreground=BLACK,
            highlightbackground="red",
            highlightthickness=2.5,
            font=MAIN_BUTTON_FONT,
            command=self.destroy_ui,
            justify=tk.CENTER,
        )
        self.exit_button.pack(expand=True)

        self.another_poetry = tk.Button(
            self.root,
            background=WHITE,
            text="Выбрать другое стихотворение",
            foreground=BLACK,
            highlightbackground="orange",
            highlightthickness=2.5,
            font=MAIN_BUTTON_FONT,
            command=self.change_poetry,
            justify=tk.CENTER,
        )

        self.first_answer_button = tk.Button(
            self.root,
            background=WHITE,
            foreground=BLACK,
            highlightbackground="green",
            highlightthickness=2.5,
            font=ANSWER_BUTTON_FONT,
            justify=tk.CENTER,
        )
        self.first_answer_button.bind("", self.next_pick)

        self.second_answer_button = tk.Button(
            self.root,
            background=WHITE,
            foreground=BLACK,
            highlightbackground="green",
            highlightthickness=2.5,
            font=ANSWER_BUTTON_FONT,
            justify=tk.CENTER,
        )
        self.second_answer_button.bind("", self.next_pick)

        self.third_answer_button = tk.Button(
            self.root,
            background=WHITE,
            foreground=BLACK,
            highlightbackground="green",
            highlightthickness=2.5,
            font=ANSWER_BUTTON_FONT,
            justify=tk.CENTER,
        )
        self.third_answer_button.bind("", self.next_pick)

        self.poetry_choices = ttk.Combobox(
            self.root, values=self.poetry_titles, font="Arial 18"
        )
        self.poetry_choices.set("Выберите стихотворение")
        self.poetry_choices.bind("<>", self.open_poetry)

        self.go_back_button = tk.Button(
            self.root,
            text="Назад",
            background=WHITE,
            foreground=BLACK,
            highlightbackground="red",
            highlightthickness=2.5,
            font=MAIN_BUTTON_FONT,
            command=self.go_back_to_menu,
            justify=tk.CENTER,
        )

Методы для создания и закрытия UI (при выходе из игры):

    def start_ui(self) -> None:
        self.root.mainloop()

    def destroy_ui(self) -> None:
        self.root.destroy()

Расположение и скрытие UI-элементов:

    @staticmethod
    def place_elements(*elements) -> None:
        for element in elements:
            element.pack(side="top", expand=2)

    @staticmethod
    def hide_elements(*elements) -> None:
        for element in elements:
            element.pack_forget()

Установка текста на кнопки для выбора ответа:

    def set_all_buttons(
        self, poetry_showing_name: str, poetry_showing: list, step: int
    ) -> None:
        self.poetry_part.set(poetry_showing[step])
      
        first_poetry = random.choice(list(poetry_dict.keys()))
        while first_poetry == poetry_showing_name:
            first_poetry = random.choice(list(poetry_dict.keys()))

        second_poetry = random.choice(list(poetry_dict.keys()))
        while second_poetry in [first_poetry, poetry_showing_name]:
            second_poetry = random.choice(list(poetry_dict.keys()))

        first_poetry = random.choice(poetry_dict[first_poetry])
        second_poetry = random.choice(poetry_dict[second_poetry])
        right_poetry = poetry_showing[step + 1]
        text = [first_poetry, second_poetry, right_poetry]

        for button in (
            self.first_answer_button,
            self.second_answer_button,
            self.third_answer_button,
        ):
            text_for_button = random.choice(text)
            button.configure(text=text_for_button)
            del text[text.index(text_for_button)]

Запуск игры (при нажатии на кнопку «Начать игру»):

    def play_game(self) -> None:
        self.hide_elements(
            self.start_label,
            self.start_button,
            self.exit_button,
            self.poetry_button,
        )

        self.poetry_showing_name = random.choice(list(poetry_dict.keys()))
        self.poetry_showing = poetry_dict[self.poetry_showing_name]
        self.last_step = len(self.poetry_showing)
        self.poetry_part.set(self.poetry_showing[self.step])
        
        self.poetry_label.configure(font="Arial 26")
        self.poetry_label.pack(fill="both", expand=True)
        self.set_all_buttons(
            poetry_showing_name=self.poetry_showing_name,
            poetry_showing=self.poetry_showing,
            step=self.step,
        )
        
        self.place_elements(
            self.first_answer_button,
            self.second_answer_button,
            self.third_answer_button,
            self.another_poetry,
            self.go_back_button,
        )

Завершение игры (при победе или поражении):

    def finish_game(self, lost: bool = False) -> None:
        self.hide_elements(
            self.first_answer_button,
            self.second_answer_button,
            self.third_answer_button,
        )
      
        self.another_poetry.configure(text="Сыграть заново")
        
        if lost:
            self.poetry_part.set(f"Упс!\n\nВы допустили ошибку.\n\nПопробуйте заново.")
        else:
            self.poetry_part.set("Поздравляю!\n\nВы полностью угадали стихотворение!")

Открытие окна «Меню со стихотворениями»:

    def open_poetry_menu(self) -> None:
        self.hide_elements(
            self.start_label,
            self.start_button,
            self.exit_button,
            self.poetry_button,
        )

        self.poetry_choices.pack(side="top", expand=2)
        self.go_back_button.pack(side="bottom", expand=2)

Подготовка для вывода текста стихотворения (в меню стихотворений):

    def build_poetry(self, poetry_rows: list) -> str:
        poetry = self.poetry_choices.get().upper() + "\n\n"
        return poetry + "\n".join(poetry_rows)

Вывод текста стихотворения при выборе элемента в комбинированном списке (Combo box):

    def open_poetry(self, _: tk.Event) -> None:
        poetry_rows = poetry_dict[self.poetry_choices.get()]
        poetry = self.build_poetry(poetry_rows)
        self.poetry_part.set(poetry)
        self.poetry_label.configure(font="Arial 16")
        self.poetry_label.pack(fill="both", expand=True)

Возвращение в основное окно (при нажатии на кнопку «Назад»):

    def go_back_to_menu(self) -> None:
        self.hide_elements(
            self.poetry_label,
            self.poetry_choices,
            self.first_answer_button,
            self.second_answer_button,
            self.third_answer_button,
            self.another_poetry,
            self.go_back_button,
        )

        self.start_label.pack(fill="both", expand=True)
        self.place_elements(self.start_button, self.poetry_button, self.exit_button)

Кнопка «Выбрать другое стихотворение» и «Сыграть заново»:

    def change_poetry(self) -> None:
        if self.another_poetry["text"] == "Сыграть заново":
            self.hide_elements(
                self.poetry_label,
                self.first_answer_button,
                self.second_answer_button,
                self.third_answer_button,
                self.another_poetry,
                self.go_back_button,
            )

            self.another_poetry.configure(text="Выбрать другое стихотворение")
            self.poetry_label.configure(font="Arial 26")
            self.poetry_label.pack(fill="both", expand=True)
            self.first_answer_button.pack(side="top", expand=2)
            self.second_answer_button.pack(side="top", expand=2)
            self.third_answer_button.pack(side="top", expand=2)
            self.another_poetry.configure(text="Выбрать другое стихотворение")
            self.another_poetry.pack(side="top", expand=2)
            self.go_back_button.pack(side="top", expand=2)

        self.poetry_showing_name = random.choice(list(poetry_dict.keys()))
        self.poetry_showing = poetry_dict[self.poetry_showing_name]
        self.step = 0
        self.set_all_buttons(
            self.poetry_showing_name,
            self.poetry_showing,
            self.step,
        )

        self.last_step = len(self.poetry_showing)

Получение текста выбранного варианта ответа:

    def get_clicked_button_text(self, button_id: str) -> str:
        return {
            "!button5": self.first_answer_button["text"],
            "!button6": self.second_answer_button["text"],
            "!button7": self.third_answer_button["text"],
        }[button_id]

Проверка ответа:

    def is_correct_answer(self, button_id: str, step: int) -> bool:
        right_answer = self.poetry_showing[step]
        picked_answer = self.get_clicked_button_text(button_id)
        if right_answer != picked_answer:
            self.finish_game(True)
            return False
        return True

Переход на следующий шаг:

    def next_pick(self, event: tk.Event) -> None:
        if self.step < (self.last_step - 2):
            self.step += 1
            if self.is_correct_answer(event.widget._name, self.step):
                self.set_all_buttons(
                    poetry_showing_name=self.poetry_showing_name,
                    poetry_showing=self.poetry_showing,
                    step=self.step,
                )
            return

        self.finish_game()

Получилась такая программа:

Игра в

Игра в «Poetry Quiz»

Подведение итогов

Конечно, всегда есть куда стремиться.

Можно было бы ещё:

  • Сделать плавнее переход между окнами

  • Покрыть код тестами

  • И много чего другого

Однако в тот момент меня всё устроило и я рад был презентовать такой необычный подарок.

Благодарю за прочтение и желаю хорошего дня! =)

© Habrahabr.ru