Пишем на Питоне игру в кости

Jazz

Опубликован:  2023-09-08T11:30:43.945887Z
Отредактирован:  2023-09-08T11:30:43.945887Z
Статус:  публичный
36
0
0

В данном случае Питоном конечно же будем считать современный высокоуровневый язык программирования общего назначения - Python3x. С появлением в компьютерной науке этого языка программирования стало очевидным, что программирование, как дисциплина, доступно любому среднестатистическому человеку, окончившему среднюю школу и умеющему читать. Учитывая обширную область применения этого ЯП, лаконичность и эффективность, я не мог не обратить на него своё пристальное внимание. В этом обзоре я расскажу, с какой стороны к Python3x подступиться и как написать на нём первую учебную программу...

Формулируем техническое задание

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

Техническое задание: необходимо разработать консольный эмулятор игры в кости. Программа должна иметь интерфейс командной строки (CLI - Command Line Interface). Программа должна обеспечивать пользователю программы возможность определять количество бросаемых игроками виртуальных кубиков, от 1 до 3, и список участников игры - один игрок или более. Если задан только один игрок, в игру должен быть автоматически добавлен второй участник - компьютер. Результаты игры (список участников с набранными каждым очками и имя победителя) программа должна выводить в терминал. Если победитель не определился в первом туре, игра должна быть продолжена между игроками, набравшими максимальное количество очков, до тех пор, пока не определится победитель.

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

Определяемся с инструментами

Для решения поставленной задачи необходим компьютер с установленной и настроенной операционной системой. Программировать на Python3x можно в любой из популярных сегодня операционных систем, но поскольку разрабатывать я буду консольную программу, именно в этом случае UNIX-way предпочтителен. В моём распоряжении есть компьютер с большим экраном, клавиатурой и с уже установленным и настроенным Debian sid, по ссылке можно обнаружить описание процесса установки и настройки используемой в этой демонстрации системы.

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

vLRxoDhucf.png

Понятно, что разрабатывая CLI приложение, я должен иметь текстовый терминал, на снимке экрана показана sakura - мой повседневный терминал, в нём я буду вводить все описанные здесь команды. Решаемая в этой демонстрации задача достаточно элементарна, и для её решения ничего, кроме стандартной библиотеки Питона, не требуется.

Любая программа обычно разрабатывается посредством создания и редактирования текстовых файлов с исходным кодом программы. Программы на Python3x не исключение, скорее правило. Это значит, что мне нужен текстовый редактор. В этой демонстрации я буду использовать текстовый редактор Vim, его графическую реализацию - GVim. Использование этого редактора невозможно без начальной теоретической подготовки хотя бы в объёме vimtutor, рекомендую для чтения на досуге.

iOp9Rd85xm.png

Vim позволяет работать с текстом не снимая рук с основной позиции на клавиатуре и без использования мыши, этим и определяется мой выбор. Если в вашей операционной системе этот текстовый редактор всё ещё не установлен, самое время его установить, в Debian основанных операционных системах это можно сделать при помощи пакетного менеджера apt.

$ sudo apt install vim gvim

Готовим окружение

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

$ mkdir -p ~/workspace/dice

Вхожу в только что созданный каталог.

$ cd ~/workspace/dice

И создаю в нём файл исполняемого модуля нашей новой программы.

$ vim -g dice.py

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

5AbZA2u6Ly.png

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

Перехожу в окно текстового редактора и пишу в нём следующий код.

#!/usr/bin/env python3

print('This is dice. I wanna play the game!')

Всего две строчки, только начало... Первая строчка этого кода - это так называемый shebang, она начинается с символа решётки, то есть является комментарием, и указывает командному интерпретатору терминала, какими именно средствами данная программа должна быть исполнена. Мы пишем программу на языке программирования Python3x, и именно его интерпретатором предлагаем исполнять эту программу.

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

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

$ python3 dice.py

66R24Ca09M.png

Как видно на снимке экрана, программа вывела в терминал переданный функции print текст в полном соответствии с начальным замыслом. Тестировать программу в процессе разработки придётся часто, и не очень удобно каждый раз при этом писать имя интерпретатора в командной строке, хочется минимизировать усилия и запускать программу самой короткой командой, для этого изменяю свойства файла dice.py.

$ chmod u+x dice.py

А затем создаю символическую ссылку на файл dice.py в одном из каталогов переменной окружения PATH, о которой в рамках этого блога я подробно расскажу чуть позже. Чтобы создать такую ссылку, мне потребуются права суперпользователя, которые можно реализовать с помощью программы sudo.

$ sudo ln -s -T /home/jazz/workspace/dice/dice.py /usr/local/bin/dice

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

$ dice

idgxoVxZbD.png

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

Задаём аргументы командной строки

Аргументы командной строки - это самое простое средство в UNIX-way передать данные от пользователя программе. Как известно, любая компьютерная программа работает с данными:

  1. Получает данные из заданного источника;

  2. Анализирует полученные данные;

  3. Обрабатывает и преобразует данные;

  4. Отдаёт итоговые данные заданному получателю.

По условиям технического задания dice должна получать от пользователя следующие данные:

  • количество виртуальных кубиков в броске;

  • список участников игры.

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

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

import argparse


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '-n', help='the loop lenght (default 3)',
        dest='loop', action='store', default=3, type=int, choices=(1, 2, 3))
    parser.add_argument(
        '-p', nargs='+', help='participants, e.g. Name1 Name2 ... NameN',
        dest='play', action='store', required=True)
    return parser.parse_args()

Здесь я импортировал модуль стандартной библиотеки argparse и, с использованием его инструментов, определил программе dice два ключа. Первый ключ -n задаёт количество виртуальных кубиков в броске каждого игрока, передаёт целое число, может иметь значение 1, или 2, или 3, не обязателен и имеет значение по умолчанию (3). Второй ключ -p задаёт список имён участников игры и обязателен для ввода.

Чтобы программа на этом этапе разработки выводила на экран хоть какую-то полезную информацию, допишу в конец кода ещё несколько строчек.

if __name__ == '__main__':
    args = parse_args()
    print('Loop: ', args.loop)
    print('Playeyrs: ', args.play)

Здесь я попросил программу вывести полученные от пользователя значения заданных ключей. Все три процедуры в этом коде заключены в так называемую стандартную идиому Питона, и будут исполнены только если программа запущена в командной строке. Такой подход даёт мне возможность импортировать объекты из модуля dice.py в интерактивной сессии и не иметь при этом ненужный выхлоп.

Сохраняю изменения в файл.

YytymyzgUw.png

Самый простой способ проверить внесённые изменения - это запуск программы в командной строке. Перехожу в окно терминала и пробую разные варианты ввода команды.

$ dice -h

Команда должна показать встроенную справку.

$ dice -n 1 -p Jazz webmaster prolinux

Команда должна показать значения переданных аргументов.

Давайте попробуем ввести заведомо ошибочные данные.

$ dice -n 8 -p Jazz webmaster

Ключ -n может иметь значения 1, или 2, или 3. Я ввёл 8. Предложенная команда выведет сообщение об ошибке. Вот как выглядит мой терминал после ввода перечисленных команд.

mKS47cBC2h.png

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

Описываем игрока

Давайте ещё раз взглянем на выхлоп dice в случае с адекватными аргументами.

$ dice -n 1 -p Jazz webmaster prolinux
Loop:  1
Playeyrs:  ['Jazz', 'webmaster', 'prolinux']

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

...
from random import randint


class Player:
    def __init__(self, name, loop):
        self.name = name
        self.score = sum(randint(1, 6) for _ in range(loop))

...

Здесь я импортировал полезную функцию randint из модуля стандартной библиотеки random, определил класс Player и в методе инициализации задал два свойства: name и score. Первое - имя игрока, второе - полученные игроком очки в результате броска заданного переменной loop количества кубиков. Не сложно догадаться, что score для каждого нового игрока будет случайной величиной в заданных границах.

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

$ python3

Терминал отзовётся вот таким выхлопом.

Python 3.11.5 (main, Aug 29 2023, 15:31:31) [GCC 13.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

OK... Мы внутри интерактивной сессии Python3x. Три угловые скобки в самом низу выхлопа обозначают приглашение, рядом с ними стоит курсор, и мы можем ввести любые процедуры Питона, получить ответ интерпретатора, и на его основе сделать выводы. Давайте попробуем поиграть с классом Player, посмотрим, что он может.

#Импортирую класс Player
>>> from dice import Player
# Создаю два экземпляра этого класса с разными именами
>>> jazz = Player('Jazz', 2)
>>> webmaster = Player('webmaster', 2)
# Получаю данные, которые каждый экземпляр хранит в своих свойствах
>>> jazz.name, jazz.score
('Jazz', 8)
>>> webmaster.name, webmaster.score
('webmaster', 4)
>>>

Каждый раз ввод должен завершаться нажатием клавиши enter клавиатуры компьютера.

tQbuu4dMG1.png

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

Описываем игру

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

...

class Game:
    def __init__(self, names, loop):
        if len(names) == 1:
            names.append('Machine')
        self.players = [Player(name, loop) for name in names]

    def count_scores(self):
        for player in self.players:
            print(f"{player.name}: {player.score}")

...

Здесь я создал ещё один класс - Game, в свойстве players этого класса хранится список экземпляров класса Player в соответствии с переданным классу Game списком имён и значением переменной loop, которая выражает количество кубиков в броске. Обращаю внимание, что каждому игроку передано одно и тоже значение loop. В соответствии с техническим заданием, если переданный список имён содержит всего одно имя, в игру будет добавлен игрок с именем Machine - компьютер.

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

bvIZo3FIuX.png

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

    def count_scores(self):
        win = max(player.score for player in self.players)
        block = max(len(i.name) for i in self.players) + len(str(win)) + 1
        for player in self.players:
            print(f"{player.name}:{player.score:>{block-len(player.name)}}")
        return [player.name for player in self.players if player.score == win]

Здесь я добавил пару переменных (win - максимальное количество очков набранное одним игроком, block - максимальная ширина строки с результатами), выровнял вывод очков по ширине на равную для каждой строчки выхлопа величину. И теперь этот метод возвращает список имён игроков, получивших максимальное количество очков.

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

ndD1DIYSMl.png

На этот раз программа выдала результаты игровой сессии в более удобочитаемой форме и вывела список игроков, получивших максимальное количество очков. Имея такой список, я могу либо вывести имя победителя, либо запустить второй тур игры, если в списке победителей больше одного имени, как того требует техническое задание. Игра описана в терминах Python3x.

Зацикливаем игровую сессии

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

if __name__ == '__main__':
    args = parse_args()
    game = Game(args.play, args.loop)
    winners = game.count_scores()
    while True:
        if len(winners) == 1:
            print(f'\n{winners[0]} is the winner.')
            break
        else:
            print('\nNo winner, play again:')
            game = Game(winners, args.loop)
            winners = game.count_scores()

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

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

tEbdM8dbaB.png

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

Код программы

Вот что у меня получилось в итоге.

dice.py

#!/usr/bin/env python3

import argparse

from random import randint


class Game:
    def __init__(self, names, loop):
        if len(names) == 1:
            names.append('Machine')
        self.players = [Player(name, loop) for name in names]

    def count_scores(self):
        win = max(player.score for player in self.players)
        block = max(len(i.name) for i in self.players) + len(str(win)) + 1
        for player in self.players:
            print(f"{player.name}:{player.score:>{block-len(player.name)}}")
        return [player.name for player in self.players if player.score == win]


class Player:
    def __init__(self, name, loop):
        self.name = name
        self.score = sum(randint(1, 6) for _ in range(loop))


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        '-n', help='the loop lenght (default 3)',
        dest='loop', action='store', default=3, type=int, choices=(1, 2, 3))
    parser.add_argument(
        '-p', nargs='+', help='participants, e.g. Name1 Name2 ... NameN',
        dest='play', action='store', required=True)
    return parser.parse_args()


if __name__ == '__main__':
    args = parse_args()
    game = Game(args.play, args.loop)
    winners = game.count_scores()
    while True:
        if len(winners) == 1:
            print(f'\n{winners[0]} is the winner.')
            break
        else:
            print('\nNo winner, play again:')
            game = Game(winners, args.loop)
            winners = game.count_scores()

Заметим, что в процессе разработки я не пытался написать всю программу сразу, а разделил процесс разработки на крохотные этапы и последовательно решал мелкие по функционалу задачи, каждый раз достигая намеченной цели, и только после этого переходил к следующему этапу разработки. Разработанная в этой демонстрации программа слишком проста, тем не менее она потребовала несколько итераций последовательной работы над кодом до достижения главной цели. Каждый этап разработки сопровождался процессом ручного тестирования полученного решения. Так чаще всего и бывает в реальной жизни разработчика на Python3x.

Выводы

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

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

  3. Программирование на Python3x требует от разработчика хорошего знания синтаксиса языка, его процедур и многих базовых модулей стандартной библиотеки.

Метки:  vim, python3x, dice, newbie, school, gvim