ПРАКТИКУМ PYTHON · ЗАНЯТИЕ 🪟

Три проекта в окне

Те же угадайка, шар и виселица — но не в консоли, а окном: с кнопками, холстом и игрой заново.

Те же проекты, но окном

Три программы, которые вы уже собирали в консоли: числовая угадайка, магический шар и виселица. Здесь они же — окнами. Придумывать заново ничего не нужно: правила игры остаются теми же, меняется только то, как программа разговаривает с человеком.

Перенос всегда идёт по одному и тому же списку. Полезно держать его перед глазами, пока пишете.

Было в консолиСтало в окне
while True — главный цикл игрыmainloop(): цикл крутит tkinter, а ход делает человек
input() — программа ждёт ответполе Entry и кнопка: ответ забирают, когда нажали
print() — строка за строкойнадписи Label: их переписывают на месте
continue — не засчитать попыткуreturn из обработчика: ничего не изменилось
break — выйти из цикла игрывыключить поле и кнопку: ходить больше нельзя
переменные внутри whileпеременные снаружи функций плюс global
«Сыграем ещё?» и новый заходкнопка «Играть заново» и функция new_game()

Обратите внимание на предпоследнюю строку: в консоли состояние игры жило внутри цикла и никуда не девалось между вопросами. В окне цикла нет — значит, всё, что программа должна помнить (загаданное число, счёт попыток, открытые буквы), переезжает наверх, к остальным переменным.

Было

while True:
    answer = input('Введите число от 1 до 100: ')
    if not is_valid(answer):
        print('Введите целое число от 1 до 100')
        continue
    ...

Стало

def guess():
    answer = field.get().strip()
    if not is_valid(answer):
        hint.config(text='Введите целое число от 1 до 100')
        return
    ...

Внешний вид, цвета и тексты сообщений можно менять на свои — важно, чтобы программа делала то, что описано в задании.

1

Числовая угадайка в окне

Программа загадывает число от 1 до 100, игрок вводит свои догадки в поле. После каждой попытки надпись сообщает, больше загаданное или меньше, а вторая надпись считает попытки.

Из чего собирается: окно, надписи, поле и кнопки (занятие 12), проверка ввода и messagebox (занятие 13), функция is_valid() из консольной версии — без единой правки.

Техническое задание.

  1. Соберите окно: надпись-подсказка, поле ввода, кнопка «Проверить», надпись со счётом попыток и кнопка «Играть заново».
  2. Заведите переменные secret и tries вне функций.
  3. Перенесите из консольной версии функцию is_valid(text) — она проверяет, что введено целое число от 1 до 100.
  4. Напишите new_game(): загадать число, обнулить счёт, вернуть надписи в исходный вид, включить поле и кнопку, очистить поле.
  5. Напишите обработчик кнопки: негодный ввод — сказать об этом и выйти, не тратя попытку; иначе увеличить счёт и сравнить с загаданным.
  6. При угадывании выключите поле и кнопку «Проверить» и покажите окошко с загаданным числом и числом попыток.
  7. Повесьте на поле клавишу Enter — играть мышью и клавиатурой одинаково удобно.
  8. Позовите new_game() один раз до mainloop(), чтобы игра началась сразу.
Три попытки сделаны, число ещё не найдено.
Три попытки сделаны, число ещё не найдено.

Что можно улучшить: показывать, сколько попыток осталось до «рекорда» прошлой партии; спрашивать верхнюю границу диапазона в отдельном поле.

Подсказка 1

Начните с new_game(): если она умеет привести окно в начальное состояние, кнопка «Играть заново» получится сама собой — она просто зовёт эту же функцию.

Подсказка 2

«Не засчитывать попытку» в окне — это return до строки tries = tries + 1. Ровно то же место, где в консоли стоял continue.

Разбор
  1. Собираем окно и объявляем переменные состояния наверху.
  2. Переносим is_valid() — она не про консоль и работает как была.
  3. Пишем new_game(): загадать, обнулить, очистить, включить.
  4. В обработчике сначала проверка ввода и ранний выход, потом счёт попытки.
  5. Сравниваем: меньше, больше или угадал; на угадывании выключаем поле с кнопкой и показываем окошко.
  6. Вешаем Enter на поле и зовём new_game() до mainloop().
Решение
import random
import tkinter
from tkinter import messagebox

secret = 0                               # загаданное число
tries = 0                                # сколько попыток сделано


def is_valid(text):
    """Годится ли введённое: целое число от 1 до 100."""
    if not text.isdigit():
        return False
    return 1 <= int(text) <= 100


def new_game():
    """Начать заново: и переменные, и вид окна."""
    global secret, tries
    secret = random.randint(1, 100)
    tries = 0
    hint.config(text='Я загадал число от 1 до 100')
    score.config(text='Попыток: 0')
    field.config(state='normal')
    field.delete(0, 'end')
    check.config(state='normal')


def guess():
    """Разобрать очередную попытку игрока."""
    global tries
    answer = field.get().strip()
    if not is_valid(answer):
        hint.config(text='Введите целое число от 1 до 100')
        return                           # попытку не засчитываем

    number = int(answer)
    tries = tries + 1
    score.config(text=f'Попыток: {tries}')

    if number < secret:
        hint.config(text='Ваше число меньше загаданного')
    elif number > secret:
        hint.config(text='Ваше число больше загаданного')
    else:
        hint.config(text='Вы угадали!')
        check.config(state='disabled')
        field.config(state='disabled')
        messagebox.showinfo('Победа',
                            f'Загаданное число: {secret}. Попыток: {tries}')


def on_enter(event):
    guess()


window = tkinter.Tk()
window.title('Числовая угадайка')

hint = tkinter.Label(window, text='Я загадал число от 1 до 100', width=32)
hint.grid(row=0, column=0, columnspan=2, padx=12, pady=12)

field = tkinter.Entry(window, width=10)
field.grid(row=1, column=0, padx=10, pady=6)
field.bind('<Return>', on_enter)

check = tkinter.Button(window, text='Проверить', command=guess)
check.grid(row=1, column=1, padx=10, pady=6)

score = tkinter.Label(window, text='Попыток: 0')
score.grid(row=2, column=0, columnspan=2)

again = tkinter.Button(window, text='Играть заново', command=new_game)
again.grid(row=3, column=0, columnspan=2, pady=10)

new_game()
window.mainloop()

Хвост «попробуйте ещё разок» из консольной версии здесь не нужен: поле и кнопка на месте, и человеку очевидно, что можно пробовать снова. Зато понадобилось то, чего в консоли не было, — выключение поля после победы: без цикла программа не заканчивается сама, и без запрета игрок продолжал бы «угадывать» уже угаданное число.

2

Магический шар в окне

Игрок пишет вопрос в поле и нажимает «Спросить» — на шаре появляется случайный ответ из двадцати. Шар рисуется на холсте: чёрный круг, блик и синий треугольник с ответом.

Из чего собирается: список ответов и random.choice() из консольной версии, поле и кнопка (занятие 12), холст и надпись на нём (занятие 14).

Техническое задание.

  1. Возьмите из консольной версии список answers из двадцати ответов — он переносится как есть.
  2. Соберите окно: приветствие, холст, поле для вопроса и кнопка «Спросить».
  3. Нарисуйте на холсте шар: большой чёрный круг, блик посветлее и синий многоугольник-окошко.
  4. Надпись с ответом сделайте фигурой холста create_text и запомните её номер — менять текст придётся именно у неё.
  5. По нажатию кнопки: если поле пустое — «Сначала задай вопрос»; иначе поставить в окошко случайный ответ и очистить поле.
  6. Повесьте на поле клавишу Enter.
Шар отвечает на вопрос.
Шар отвечает на вопрос.

Что можно улучшить: показывать заданный вопрос над шаром; заводить задержку в полсекунды перед ответом через after — будто шар думает.

Подсказка 1

Текст фигуры на холсте меняется не через config, а через canvas.itemconfig(номер, text='…') — номер вернул create_text, когда фигуру создавали.

Подсказка 2

Чтобы длинный ответ не уехал за края шара, у create_text есть свойство width — ширина в точках, по которой текст переносится на другую строку.

Разбор
  1. Переносим список ответов из консольной версии.
  2. Ставим холст и рисуем на нём круг, блик и треугольник — именно в таком порядке, чтобы каждый следующий лёг поверх.
  3. Создаём create_text в середине треугольника и сохраняем номер в переменную.
  4. В обработчике проверяем, что вопрос не пустой.
  5. Ставим случайный ответ в надпись холста и чистим поле.
Решение
import random
import tkinter

# двадцать граней настоящего шара судьбы
answers = ['Бесспорно',
           'Предрешено',
           'Никаких сомнений',
           'Определённо да',
           'Можешь быть уверен в этом',
           'Мне кажется — да',
           'Вероятнее всего',
           'Хорошие перспективы',
           'Знаки говорят — да',
           'Да',
           'Пока неясно, попробуй снова',
           'Спроси позже',
           'Лучше не рассказывать',
           'Сейчас нельзя предсказать',
           'Сконцентрируйся и спроси опять',
           'Даже не думай',
           'Мой ответ — нет',
           'По моим данным — нет',
           'Перспективы не очень хорошие',
           'Весьма сомнительно']


def ask():
    """Ответить на вопрос игрока случайной фразой."""
    question = field.get().strip()
    if question == '':
        canvas.itemconfig(bubble, text='Сначала задай вопрос')
        return
    canvas.itemconfig(bubble, text=random.choice(answers))
    field.delete(0, 'end')


def on_enter(event):
    ask()


window = tkinter.Tk()
window.title('Магический шар')

hello = tkinter.Label(window,
                      text='Я магический шар и знаю ответ на любой вопрос')
hello.grid(row=0, column=0, columnspan=2, padx=12, pady=10)

canvas = tkinter.Canvas(window, width=260, height=200, bg='#101018')
canvas.grid(row=1, column=0, columnspan=2, padx=12)

canvas.create_oval(30, 10, 230, 190, fill='black', outline='#333344')
canvas.create_oval(70, 40, 120, 70, fill='#2a2a3a', outline='')   # блик
canvas.create_polygon(130, 60, 200, 130, 130, 175, 60, 130,
                      fill='#1b2a6b', outline='#22337f')
bubble = canvas.create_text(130, 120, text='Задай мне вопрос',
                            fill='white', width=110, justify='center')

field = tkinter.Entry(window, width=26)
field.grid(row=2, column=0, padx=10, pady=12)
field.bind('<Return>', on_enter)

button = tkinter.Button(window, text='Спросить', command=ask)
button.grid(row=2, column=1, padx=10, pady=12)

window.mainloop()

Сам вопрос программе по-прежнему не нужен — она его не читает, как и консольная версия. Но проверить, что он не пустой, стоит: шар, который отвечает на пустоту, выглядит сломанным, а не таинственным.

3

Виселица в окне

Программа загадывает слово, игрок называет буквы по одной. Верная буква открывается во всех местах слова, неверная добавляет часть человечка на холсте. Частей шесть: голова, туловище, две руки, две ноги.

Из чего собирается: списки и работа со строкой из консольной версии, состояние игры и messagebox (занятие 13), холст с фигурами, которые можно прятать и показывать (занятие 14).

Техническое задание.

  1. Возьмите список слов из консольной версии; слова пишутся заглавными буквами.
  2. Заведите переменные: загаданное слово, список открытых букв, список названных букв, число ошибок.
  3. Соберите окно: холст, надпись со словом, поле для буквы, кнопка «Назвать букву», надписи с подсказкой, названными буквами и счётом ошибок, кнопка «Играть заново».
  4. Нарисуйте на холсте виселицу, а шесть частей человечка создайте заранее и спрячьте (state='hidden'), сложив их номера в список.
  5. Напишите show_word(): буквы, которые открыты, печатаются, остальные — прочерком, через пробел.
  6. Напишите new_game(): новое слово, пустые списки, ноль ошибок, все части спрятаны, поле и кнопка включены.
  7. В обработчике: не одна буква — сказать и выйти; уже называли — сказать и выйти; ошибку в обоих случаях не засчитывать.
  8. Верная буква — открыть её и проверить, не собрано ли слово целиком. Неверная — показать очередную часть человечка и увеличить счёт ошибок.
  9. Победа и проигрыш заканчиваются одинаково: выключить поле с кнопкой и показать окошко с итогом.
Две ошибки: у человечка голова и туловище.
Две ошибки: у человечка голова и туловище.

Что можно улучшить: по кнопке открывать первую и последнюю буквы; вести счёт побед и поражений за все партии.

Подсказка 1

Части человечка удобнее создать один раз при сборке окна и прятать, чем рисовать и стирать: тогда «Играть заново» — это просто спрятать все шесть обратно.

Подсказка 2

Проверка «слово собрано» — булева функция из одиннадцатого занятия: пройти по буквам слова и вернуть False, как только встретится неоткрытая.

Разбор
  1. Собираем окно и рисуем виселицу; части человечка складываем в список и прячем.
  2. show_word() собирает строку из открытых букв и прочерков.
  3. new_game() приводит в начало и переменные, и окно — её же зовёт кнопка «Играть заново».
  4. В обработчике сначала две проверки с ранним выходом: одна ли буква и не называли ли её.
  5. Верная буква — в список открытых, перерисовать слово, проверить победу.
  6. Неверная — показать часть parts[mistakes - 1], увеличить счёт, на шестой ошибке закончить партию.
Решение
import random
import tkinter
from tkinter import messagebox

word_list = ['ПАРТИЯ', 'КОЛЬЦО', 'РАДИО', 'ПРАВДА', 'ИНФОРМАТИКА']

word = ''                                # загаданное слово
opened = []                              # какие буквы уже открыты
used = []                                # какие буквы называли
mistakes = 0                             # сколько ошибок сделано
parts = []                               # части человечка на холсте


def build_man():
    """Создать части человечка заранее и спрятать их."""
    parts.append(canvas.create_oval(120, 55, 160, 95, width=2))
    parts.append(canvas.create_line(140, 95, 140, 150, width=2))
    parts.append(canvas.create_line(140, 110, 110, 130, width=2))
    parts.append(canvas.create_line(140, 110, 170, 130, width=2))
    parts.append(canvas.create_line(140, 150, 115, 190, width=2))
    parts.append(canvas.create_line(140, 150, 165, 190, width=2))
    for part in parts:
        canvas.itemconfig(part, state='hidden')


def show_word():
    """Переписать слово: открытые буквы, остальные — прочерки."""
    line = ''
    for letter in word:
        if letter in opened:
            line = line + letter + ' '
        else:
            line = line + '_ '
    board.config(text=line.rstrip())


def is_won():
    """Всё ли слово открыто."""
    for letter in word:
        if letter not in opened:
            return False
    return True


def finish(text, title, message):
    """Закончить партию: сказать итог и запретить ходить."""
    hint.config(text=text)
    field.config(state='disabled')
    button.config(state='disabled')
    messagebox.showinfo(title, message)


def new_game():
    """Новая партия: новое слово, чистый холст, включённые кнопки."""
    global word, opened, used, mistakes
    word = random.choice(word_list)
    opened = []
    used = []
    mistakes = 0
    for part in parts:
        canvas.itemconfig(part, state='hidden')
    field.config(state='normal')
    field.delete(0, 'end')
    button.config(state='normal')
    hint.config(text='Назовите букву')
    letters.config(text='Названные буквы: —')
    left.config(text='Ошибок: 0 из 6')
    show_word()


def guess():
    """Разобрать названную букву."""
    global mistakes
    letter = field.get().strip().upper()
    field.delete(0, 'end')

    if len(letter) != 1 or not letter.isalpha():
        hint.config(text='Назовите одну букву')
        return                           # ошибку не засчитываем
    if letter in used:
        hint.config(text='Эту букву уже называли')
        return

    used.append(letter)
    letters.config(text='Названные буквы: ' + ' '.join(used))

    if letter in word:
        opened.append(letter)
        show_word()
        hint.config(text=f'Буква {letter} есть в слове')
        if is_won():
            finish('Вы победили!', 'Победа', f'Слово: {word}')
        return

    mistakes = mistakes + 1
    canvas.itemconfig(parts[mistakes - 1], state='normal')
    left.config(text=f'Ошибок: {mistakes} из 6')
    hint.config(text=f'Буквы {letter} в слове нет')
    if mistakes == 6:
        finish('Попытки кончились', 'Проигрыш', f'Загаданное слово: {word}')


def on_enter(event):
    guess()


window = tkinter.Tk()
window.title('Угадайка слов')

canvas = tkinter.Canvas(window, width=280, height=220, bg='white')
canvas.grid(row=0, column=0, columnspan=2, padx=12, pady=10)

canvas.create_line(40, 200, 100, 200, width=3)      # основание
canvas.create_line(70, 200, 70, 30, width=3)        # столб
canvas.create_line(70, 30, 140, 30, width=3)        # перекладина
canvas.create_line(140, 30, 140, 55, width=2)       # верёвка
build_man()

board = tkinter.Label(window, text='', font=('Consolas', 16))
board.grid(row=1, column=0, columnspan=2, pady=6)

field = tkinter.Entry(window, width=6)
field.grid(row=2, column=0, padx=10, pady=8)
field.bind('<Return>', on_enter)

button = tkinter.Button(window, text='Назвать букву', command=guess)
button.grid(row=2, column=1, padx=10, pady=8)

hint = tkinter.Label(window, text='Назовите букву')
hint.grid(row=3, column=0, columnspan=2)

letters = tkinter.Label(window, text='Названные буквы: —')
letters.grid(row=4, column=0, columnspan=2)

left = tkinter.Label(window, text='Ошибок: 0 из 6')
left.grid(row=5, column=0, columnspan=2)

again = tkinter.Button(window, text='Играть заново', command=new_game)
again.grid(row=6, column=0, columnspan=2, pady=10)

new_game()
window.mainloop()

Номер части берётся как parts[mistakes - 1], потому что список нумеруется с нуля, а ошибки считаются с единицы: первая ошибка показывает parts[0]. Это то самое место, где легче всего ошибиться на единицу — и заметно это сразу: либо голова не появляется, либо на шестой ошибке программа падает.

Итог занятия

Что дальше. Эти три программы уже можно показывать: они открываются двойным щелчком, не требуют объяснений и не пугают чёрным окном консоли. Дальше их обычно доводят по мелочам — своя картинка вместо серого фона, счёт партий, подсказки, — и именно мелочи отличают программу, которую не стыдно дать в руки другому человеку, от «работает у меня».

Тот же перенос делается с любой консольной задачей курса: калькулятор, перевод величин, проверка пароля. Список из первой панели — «было в консоли, стало в окне» — работает для всех.