Создайте свою первую игру — «камень, ножницы, бумага»!

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

Парни играют в "камень, ножницы, бумага"

Следуя этому руководству, вы:

  • напишете собственную игру «камень, ножницы, бумага»  
  • научитесь принимать данные, введенные пользователем
  • узнаете, как навести порядок в своем коде с помощью enum и функций.

Что такое «камень, ножницы, бумага»?  

Скорее всего, вы уже играли в эту игру. Возможно, с ее помощью вы решали, кто заплатит за ужин, или использовали ее для жеребьевки. 

Если же вам эта игра не знакома, то вот краткое описание. «Камень, ножницы, бумага» — это игра на руках для двух и более людей. Участники говорят: «Камень, ножницы, бумага!». После этого они одновременно придают своим рукам форму камня (кулак), листа бумаги (ладонь вниз) или ножниц (два вытянутых пальца). Правила просты:

  • камень бьет ножницы
  • бумага оборачивает камень
  • ножницы режут бумагу.

Теперь вы знакомы с правилами — пора подумать, как преобразовать их в код на Python!

Ваша первая партия в «камень, ножницы, бумага» 

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

 import random

Отлично! Теперь вы сможете использовать методы модуля random, чтобы имитировать противника. Что дальше? Так как пользователям нужно тоже иметь возможность совершать какие-то действия, нам нужно реализовать пользовательский ввод. 

Ввод пользовательских данных

Ввод пользовательских данных в Python довольно прост. Он нужно для того, чтобы пользователь сделал выбор действия. После этого выбор записывается в переменную.

 user_action = input("Сделайте выбор — камень, ножницы или бумага: ")

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

Подключаем к игре компьютер

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

Чтобы заставить компьютер выбрать случайное действие из предложенных, мы будем использовать random.choice():

possible_actions = ["камень", "бумага", "ножницы"]
computer_action = random.choice(possible_actions)

Это позволяет выбрать случайный элемент из списка действий. Также вы можете вывести в консоль варианты, которые сделали пользователь и компьютер:

print(f"\nВы выбрали {user_action}, компьютер выбрал {computer_action}.\n")

Вывод этой информации в консоль может быть полезным пользователю. К тому же, вы ее можете использовать использовать для отладки. Вдруг что-то пойдет не так?

Определяем победителя

Оба игрока сделали свой ход — осталось выбрать победителя. Сделать это можно с помощью блока if-elif-else. Именно это позволит вам сравнить фигуры игроков и определить победителя:

if user_action == computer_action:
    print(f"Оба пользователя выбрали {user_action}. Ничья!!")
elif user_action == "камень":
    if computer_action == "ножницы":
        print("Камень бьет ножницы! Вы победили!")
    else:
        print("Бумага оборачивает камень! Вы проиграли.")
elif user_action == "бумага":
    if computer_action == "камень":
        print("Бумага оборачивает камень! Вы победили!")
    else:
        print("Ножницы режут бумагу! Вы проиграли.")
elif user_action == "ножницы":
    if computer_action == "бумага":
        print("Ножницы режут бумагу! Вы победили!")
    else:
        print("Камень бьет ножницы! Вы проиграли.")

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

Играем несколько партий подряд

Да, сыграть партейку в «камень, ножницы, бумага» — это, конечно, очень весело. Но не лучше ли сыграть несколько? Циклы — отличный способ создать повторяющиеся события. Например, while позволит нам играть бесконечно:

import random

while True: # обратите внимание на эту строку кода
    user_action = input("Сделайте выбор — камень, ножницы или бумага: ")
    possible_actions = ["камень", "бумага", "ножницы"]
    computer_action = random.choice(possible_actions)
    print(f"\nВы выбрали {user_action}, компьютер выбрал {computer_action}.\n")

    if user_action == computer_action:
        print(f"Оба пользователя выбрали {user_action}. Ничья!!")
    elif user_action == "камень":
        if computer_action == "ножницы":
            print("Камень бьет ножницы! Вы победили!")
        else:
            print("Бумага оборачивает камень! Вы проиграли.")
    elif user_action == "бумага":
        if computer_action == "камень":
            print("Бумага оборачивает камень! Вы победили!")
        else:
            print("Ножницы режут бумагу! Вы проиграли.")
    elif user_action == "ножницы":
        if computer_action == "бумага":
            print("Ножницы режут бумагу! Вы победили!")
        else:
            print("Камень бьет ножницы! Вы проиграли.")
# а также обратите внимание на код ниже
    play_again = "" 
    play_again = input("Сыграем еще? (д/н): ") 
    if play_again.lower() != "д": 
        break 

В приведенном выше коде оставлены комментарии, обращающие ваше внимание на определенные строки. Давайте их разберем.

Очень важно проверить, не хочет ли пользователь сыграть еще одну партию. И, конечно же, закрыть игру, если пользователь наигрался. Без этой проверки пользователь будет вынужден играть до тех пор, пока он не закроет консоль вручную — с помощью Ctrl+C. 

Проверка осуществляется с помощью строки “д”. Но это может вызвать у пользователя трудности — вдруг он введет “да” или “нет”. Проверка строки — дело хитрое, ведь вы никогда точно не узнаете, что пользователь может ввести в консоль. Например, трудно предугадать, в каком регистре будут буквы. 

Пример сравнения нескольких строк:

>>> play_again = "да"
>>> play_again == "н"
False
>>> play_again != "д"
True

Хмм… это не совсем то, что нам нужно. Будет грустно, если пользователь введет “да”, а его выкинет из игры. 

Использование enum.IntEnum для хранения действий

Сравнение строк, как вы могли заметить, может вызвать проблемы. Именно поэтому нам нужно постараться этого избежать. Но ведь первое, что спрашивает программа, — это строка! А что, если пользователь введет “Камень” или “кАмень”? Регистр — это очень важно: 

>>> print("камень" == "Камень")
False

Поскольку регистр имеет значение, то “к” и “К” — не одно и то же. Одно из возможных решений — использование чисел. Привязка каждой фигуры к числу может уберечь вас от множества проблем:

 ROCK_ACTION = 0 # камень
 PAPER_ACTION = 1 # бумага
 SCISSORS_ACTION = 2 # ножницы

Сравнение целых чисел не вызывает тех же проблем, так что возьмем это решение на вооружение. Теперь наш пользователь будет вводить числа, а сравнивать мы будем два значения: 

user_input = input("Сделайте выбор — (камень[0], бумага[1], ножницы[2]): ")
user_action = int(user_input)
if user_action == ROCK_ACTION:
    # блок кода для ROCK_ACTION

Но input() возвращает строку! Нам нужно привести введенное значение в целочисленный тип с помощью int(). Работает это хорошо, но нужно объявлять переменные с осторожностью: иначе сравнение будет затруднительно. В данном случае для объявления класса с действиями лучше всего использовать enum.IntEnum

С помощью enum.IntEnum вы можете создать атрибуты и связать с ними какое-либо значение — точно так же, как в примере выше. Так вы сгруппируете все действия в их собственных пространствах имен, в результате чего код станет чище и приятнее на вид.

from enum import IntEnum

class Action(IntEnum):
    Rock = 0
    Paper = 1
    Scissors = 2

Мы создали класс Action, в котором описали все действия. То есть, привязали каждое действие к числу. 

Сравнение все еще очень простое — но сейчас у нас есть класс, связанный с действиями: 

>>> Action.Rock == Action.Rock
True

Так как значения одинаковы, результатом будет True. Имена, которые мы использовали, делают сравнение еще более очевидным.

Action можно использовать даже с int:

>>> Action.Rock == Action(0)
True
>>> Action(0)
<Action.Rock: 0>

Action проверяет значение, которое мы передали, и возвращает соответствующее действие. Это полезно, ведь мы теперь можем принимать от пользователя даже целые числа и сопоставлять их с соответствующими действиями. Никаких беспокойств!

Блок-схема вашей программы

Хотя игра «камень, ножницы, бумага» кажется несложной, невероятно важно разобрать каждую деталь игры и все возможные сценарии. В любом проекте, даже в самом маленьком, блок-схема — незаменимый инструмент. По нему строят логику, пишут код. Вы можете получить тот же результат, используя маркированный список. Но у такого метода есть и недостаток — с его помощью сложнее охватить вещи вроде циклов и условной логики. 

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

Ниже представлена блок-схема, описывающая партию «камень, ножницы, бумага»:

Блок-схема: "получаем выбор пользователя" - "Получаем выбор компьютера" - "Определяем победителя"

Каждый пользователь выбирает действие, а затем определяется победитель. Эта блок-схема описывает партию лишь в том виде, в котором мы ее запрограммировали. С реальной игрой она не имеет ничего общего. В реальной жизни игроки выбирают действия одновременно, а не по очереди, как показано на этой блок-схеме. 

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

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

Блок-схема с зацикливанием

Без блок-схемы мы бы не поняли, что нужен функционал для начала новой партии. Главная суть схем в том, что они позволяют исправить ошибки еще до написания кода, благодаря чему ваш код в итоге будет более грамотным и аккуратным. 

Разбиваем код на функции

Итак, мы описали работу нашей программы с помощью блок-схемы. Теперь мы можем отредактировать свой код так, чтобы он максимально точно ей соответствовал. Начать можно с разбиения кода на функции по принципу «один шаг — одна функция». Функции — удобный способ разделить большие куски кода на более мелкие, которые проще редактировать.

Создавать отдельную функцию для проверки, хочет ли пользователь сыграть снова, необязательно, но это не будет лишним. Начать можно с импорта random, если вы этого до сих пор не сделали, и объявления класса Action:

import random
from enum import IntEnum

class Action(IntEnum):
    Rock = 0
    Paper = 1
    Scissors = 2

Надеемся, выглядит знакомо. А теперь приступим к функции get_user_selection(), которая не принимает никаких аргументов и возвращает Action

def get_user_selection():
    user_input = input("Сделайте выбор — (камень[0], бумага[1], ножницы[2]): ")
    selection = int(user_input)
    action = Action(selection)
    return action

Обратите внимание на то, что пользовательский ввод мы принимаем в виде int, а возвращаем уже Action. Сообщение, конечно, выглядит довольно громоздко. Вдруг мы захотим добавить больше действий? Нам тогда придется его расширять. 

Вместо этого можно использовать генераторы списков:

def get_user_selection():
    choices = [f"{action.name}[{action.value}]" for action in Action]
    choices_str = ", ".join(choices)
    selection = int(input(f"Сделайте выбор — ({choices_str}): "))
    action = Action(selection)
    return action

Теперь мы можем не беспокоиться о том, что в будущем придется добавлять или убирать какие-то действия.

Давайте проверим, как это работает! Сначала программа запрашивает ввод у пользователя, а потом возвращает выбранное им действие.

>>> get_user_selection()
Сделайте выбор — (камень[0], бумага[1], ножницы[2]): 0
<Action.Rock: 0>

Теперь нужно написать функцию, в которой компьютер будет делать свой выбор. По аналогии с get_user_selection(), эта функция не принимает аргументов и возвращает Action. Так как диапазон значений выбора от 0 до 2, выбирать случайные значения мы будем из этого промежутка. В этом нам поможет random.randint().

random.randint() возвращает случайное значение из заданного диапазона. С помощью len() можно очень легко определить верхнюю границу:

def get_computer_selection():
    selection = random.randint(0, len(Action) - 1)
    action = Action(selection)
    return action

Поскольку значения начинаются с 0, а счет len() — с 1, следует вычесть единицу из len(Action).

Если вы запустите этот кусочек кода, то приглашения к вводу не будет. Эта функция лишь вернет действие, связанное со случайным числом:

>>> get_computer_selection()
<Action.Scissors: 2>

Неплохо! Теперь нам нужно определить победителя. Эта функция принимает два аргумента — выбор пользователя и компьютера. Она не должна ничего возвращать, от нее нужен лишь вывод результата сравнения в консоль:

def determine_winner(user_action, computer_action):
    if user_action == computer_action:
        print(f"Оба пользователя выбрали {user_action.name}. Ничья!")
    elif user_action == Action.Rock:
        if computer_action == Action.Scissors:
            print("Камень бьет ножницы! Вы победили!")
        else:
            print("Бумага оборачивает камень! Вы проиграли.")
    elif user_action == Action.Paper:
        if computer_action == Action.Rock:
            print("Бумага оборачивает камень! Вы победили!")
        else:
            print("Ножницы режут бумагу! Вы проиграли.")
    elif user_action == Action.Scissors:
        if computer_action == Action.Paper:
            print("Ножницы режут бумагу! Вы победили!")
        else:
            print("Камень бьет ножницы! Вы проиграли.")

Эта функция очень похожа на то, что мы писали раньше. Но сейчас нам не нужно беспокоиться из-за капризных строк — сравнение происходит напрямую.

Можем передать в эту функцию разные аргументы и посмотреть на результат:

>>> determine_winner(Action.Rock, Action.Scissors)
Камень бьет ножницы! Вы победили!

Что произойдет, если пользователь введет 3? Ведь самое больше объявленное число это 2 :

>>> Action(3)
ValueError: 3 is not a valid Action

Упс! Вы точно не хотите, чтобы это произошло. Вернемся к блок-схеме. Куда мы можем добавить логику, проверяющую правильность ввода?

Логичным будет проверить это сразу после ввода пользователя:

Блок-схема

Если пользователь введет число, выходящее за пределы указанного диапазона, мы просто повторим 1 шаг. Единственное требование для пользователя — число должно быть от 0 до 2 включительно. Если ввод пользователя выходит за эти пределы, возбуждается исключение ValueError. Чтобы этого избежать, исключение нужно обработать.

Итак, мы объявили функции, которые отражают все шаги из блок-схемы. Логика теперь гораздо компактнее. Теперь все, что включает в себя цикл while, это:

while True:
    try:
        user_action = get_user_selection()
    except ValueError as e:
        range_str = f"[0, {len(Action) - 1}]"
        print(f"Некорректный ввод. Введите значение из промежутка {range_str}")
        continue

    computer_action = get_computer_selection()
    determine_winner(user_action, computer_action)

    play_again = input("Сыграем еще? (д/н): ")
    if play_again.lower() != "д":
        break

Выглядит гораздо лучше, не так ли? Заметьте, что программа продолжает работу, даже если пользователь вводит некорректное число.