Это первая статья из серии статей, в которой описывается опыт написания с нуля библиотеки на питоне, для расчета как можно более широкого спектра деловых, производственных, организационных задач методами теории игр.
Это пет-проект автора статьи.
Сформулируем функциональные требования к библиотеке
Здесь описан максимальный объем требований, пока реализована лишь малая часть. Кроме того, перечень требований будет периодически корректироваться (корректировки будут показаны).
Функциональные требования для разработки библиотеки для постановки, анализа и решения широкого спектра бизнес- и производственных задач методами теории игр
-
В целом библиотека должна давать нам возможность ввести максимально разнообразные условия, развилки и зависимости, которые возникают в игровых ситуациях (в т.ч. описание стратегий игроков), без привлечения программирования (разработки алгоритмов). Здесь мы понимаем, что работа с библиотекой «без программирования» — это последовательный вызов функций создания классов с определенными атрибутами, и методов этих классов с определенными атрибутами (либо такой вызов путем использования интерфейса, в т.ч. веб-интерфейса). Написание же кода с операторами if, for, while, созданием функций считается «программированием», то есть разработкой алгоритма.
-
В то же время, библиотека должна позволять использовать написанные пользователем функции вместо типовых функций библиотеки.
-
Для того, чтобы библиотека не представляла собой «черного ящика» для пользователей, она должна быть открытой, код должен быть доступен на гитхаб, в т.ч. для форков и доработок.
-
Код библиотеки может быть написан на разных языках, но предпочтительным является python из-за доступности, большого количества вспомогательных библиотек и возможности быстро создавать и запускать новый код в большом количестве сред.
-
Код должен быть документирован, описаны все классы, параметры и функции/методы. Для каждой функции, метода, создания экземпляра класса должны быть приведены минимум два примера, имеющих отношение к практическим деловым, маркетинговым, организационным ситуациям.
-
Каждая сущность должна быть отдельным классом со своими атрибутами. Должна быть понятна и логична структура и взаимосвязь классов библиотеки.
-
Входной и выходной информации может быть много и она должа быть упорядочена. Поэтому библиотека должна поддерживать ORM — объектно-реляционную модель. Экземлпяры классов должны сохраняться в реляционной базе данных в виде таблиц с автоматически формируемыми связями между ними (как минимум, должна поддерживаться SQLite и PostgreSQL).
-
Библиотека должна быть дружелюбна к потенциальным оболочкам (интерфейсам, в том числе веб-интерфейсам), в которых должны поддерживаться все классы, их аргументы и методы.
-
Между экземплярами классов должна поддерживаться произвольная связь минимум двух видов: группировка по какому-либо признаку и отношения «родитель-потомок» для того, чтобы мы могли быстро находить в базе данных все составляющие игры, видеть какие игры, стратегии являются прародителем или развитием других игр, стратегий и т.п.
-
Должны присутствовать генераторы данных, то есть можно быстро создать группу экземлпяров класса, в которых определенные параметры меняются в определенных пределах. Должны поддерживаться расчеты множества игр с этими параметрами и выводиться сводная статистика (например, мы сможем сыграть множество игр, в которых соотношение выигрышей в стратегиях А и B изменяется от 0.1 до 0.9 с шагом 0.01, а количество игроков — от 2 до 100, и оценить изменение эффективности определенной стратегии при изменении определенных параметров).
-
Должны поддерживаться максимально быстрые алгоритмы расчета Игры. При наличии теоретически обоснованной формулы — используется данная формула, в противном случае — проводится достаточно большое количество игр, и из статистики делаются заключения.
-
Класс Игрок должен позволять использовать максимально широкое количество стратегий путем задания их параметров, как без непосредственного программирования, так и с включением пользовательских функций.
-
Количество Игроков может изменяться от 0 до «неопределенно большой величины», в т.ч. может быть переменным в различных раундах Игры.
-
Должна поддерживаться возможность анализа Игры с точки зрения оптимизации выигрыша выбранного Игрока или группы Игроков.
-
Игры могут быть одновременными (Игроки ходят одновременно) или последовательными (Игроки ходят по очереди), либо могут сочетать одновременность и последовательность, исходя из какого-либо условия, в т.ч. по выбору Игроков. При последовательной игре очередь (порядок хода) Игроков может меняться по разным правилам. При одновременной игре возможен вариант одной или нескольких «переторжек», когда Игроки могут поменять свой выбор, в т.ч. последовательной переторжки (Игроки меняют первоначальный выбор по очереди).
-
Игры могут играться любое число раундов (последовательных игр), в т.ч. количество раундов может зависеть от определенных правил, условий.
-
Платежная матрица должна поддерживать возможность получения Игроками положительных или отрицательных числовых выигрышей и (или) варианта получения или отдачи Игроками разных Ресурсов.
-
Ресурсы в игре могут быть «материальными», которые могут быть ограничены некоторым запасом в игре или в каждом раунде, возможностью хранения у Игрока, и «нематериальными». Ресурсы могут иметь разные и изменяющиеся по ходу игры соотношения ценности, в т.ч. ценности Ресурсов могут различаться для разных Игроков, а для одного Игрока ценность (полезность) Ресурса может зависеть как от его количества, так и от наличия определенного количества иных определенных Ресурсов (взаимодополняющие Ресурсы).
-
Должна быть возможность для игрока опции «вступать или не вступать в Игру», в т.ч. либо вообще, либо в определенном раунде. Должна быть возможность назначить для Игрока плату (либо вознаграждение) за вступление в Игру, вообще или в определенном раунде, в том числе в зависимости от количества Игроков, выбравших Игру.
-
Варианты или «опции», которые могут выбирать Игроки, могут меняться в зависимости от номера хода или иметь сетевую структуру (например, при выборе хода А далее можно выбирать C или D, а при выборе B — далее только D). За выбор той или иной опции может взиматься плата, в т.ч. в зависимости от количества Игроков, выбравших опцию.
-
Стратегиями будут называться в общем случае алгоритмы, включающие как постоянный выбор определенной опции, так и варианты сложного алгоритма выбора той или иной опции в зависимости от фактического или ожидаемого выбора иных Игроков в том или ином раунде, иных параметров Игры и Игроков. Должен поддерживаться расчет равновесных стратегий (по Нэшу), Парето-оптимальных стратегий, максимизации выигрыша в отношении определенного Игрока или группы Игроков.
-
Должна быть возможность задавать различные параметры целевой функции каждого Игрока, в т.ч. моделирующие параметры, используемые в бизнесе и для оценки проектов (в т.ч. прибыль, рентабельность, NPV, IRR, срок окупаемости и т.д., либо абсолютный или относительный прирост этих показателей).
-
Игроки могут вступать или не вступать в коалиции, либо быть изначально приписаны определенной коалиции (цеху). Коалиция может обладать собственными Ресурсами, в т.ч. получать Ресурсы при определенном выборе Игроков. Должна быть возможность учета интересов коалиции в случае поиска наилучших стратегий.
-
В игре должны быть доступны различные варианты случайного выбора максимального количества составляющих Игры (например, случайное количество опций, доступных в раунде, случайное количество ресурсов, доступных при выборе опции, случайное количество игроков в раунде и т.д.). Должна быть возможность задавать риски, как вероятности потери Ресурсов или получения отрицательной полезности.
-
Должна быть возможность введения неполной информации — возможности задавать для разных Игроков разные уровни знания (наличие знания, искажённое знание) относительно параметров Игры (например, вероятности получения того или иного Ресурса при выборе опции, знание стратегии других Игроков и т.п.)
-
Библиотека должна быть дружелюбна к распространенным пакетам ML, включая нейронные сети: в идеале, нейронная сеть должна уметь по текстовому описанию распознавать игровые паттерны (кто игроки, каковы параметры игры, за какие ресурсы идет игра, какие опции существуют в игре, какие как оцениваются различные вероятности и т.п.), и находить оптимальные решения.
-
По ходу разработки библиотеки и обсуждения классов, их атрибутов и методов, возможны внесения изменений и уточнений в данные Требования, а также изменения приоритетности тех или иных требований.
Начинаем разработку библиотеки — реализуем класс-миксин
Миксины — это классы, которые «подмешиваются» к основным классам, добавляя им некоторую общую функциональность, чтобы не дублировать эту функциональность в каждом классе.
Наш миксин должен поддерживать:
-
Создание уникального имени для каждого экземпляра класса. Уникальное имя нам нужно для того, чтобы отличать этот экземпляр от других и хранить его в словаре в виде ключа (все ключи в словаре должны быть уникальны). Уникальность будем обеспечивать добавлением суффикса «_n», где n — уникальное число по-порядку. Если имя не задано, то по умолчанию создается уникальное, например, для класса
Player
будет создано «Player_0″ и т.д. -
Создание описания для экземпляра каждого класса. Это произвольная текстовая строка, описывающая особенности экземпляра. По умолчанию там будут название класса и список атрибутов со значениями, кроме самого атрибута описания.
-
Добавление в атрибут класса — словарь
all
{имя_экземпляра: ссылка_на_экземпляр}
. Это нужно для того, чтобы все созданные экземпляры хранились в словаре, и мы могли бы их быстро найти по имени, записать в базу данных и т.п.
class Mixin(): """Позволяет создавать уникальное имя для экземпляра любого класса и добавлять имя в словарь .all Входные параметры: name - имя экземпляра класса. Если не задано, создается уникальное, например, для класса Player будет создано Player_0 и т.д. about - произвольное описание, строка. Если не задано, включается имя класса и значения параметров. При создании экземпляра класса его имя добаляется в атрибут класса .all, представляющий собой словарь {name: ссылка на экземпляр} """ def __init__(self, name: str = None, about: str = None, **kwargs): object_name = f'{type(self).__name__}' object_suffix = 0 is_suffix_missing = False if name is None: # Создадим уникальное имя, которого нет в словаре all класса # в формате класс_n, если name уже существует, добавим суффикс while is_suffix_missing is False: if f'{object_name}_{object_suffix}' in type(self).all: object_suffix += 1 else: is_suffix_missing = True self.name = f'{object_name}_{object_suffix}' else: self.name = name if self.name in type(self).all: while is_suffix_missing is False: if self.name in type(self).all: object_suffix += 1 self.name = f'{name}_{object_suffix}' else: is_suffix_missing = True self.about = about if about is None: tmp_about = f'Class: {object_name}' for key, value in self.__dict__.items(): if key == 'about': continue else: tmp_about += f'/ {key}: {value}' self.about = tmp_about type(self).all[self.name] = self
Более подробные пояснения к коду класса-миксина
def __init__(self, name: str = None, about: str = None, **kwargs):
__init__(self)
— это функция, которая вызывается при создании экземпляра класса. Мы не создаем экземпляры класса Mixin
, но будем вызывать эту функцию из других классов, чтобы она добавляла имя и описание каждому классу.
**kwargs
— это словарь произвольных именованных атрибутов (атрибут: имя), они передаются из наших основных классов, чтобы мы записали их в атрибут about
.
type(self)
позволяет нам понимать, с каким классом мы работаем, в частности, type(self).name
дает нам имя класса.
type(self).all[self.name] = self
Здесь мы записываем в атрибут нашего класса (не экземпляра!) all
, представляющий собой словарь, {имя_экземпляра: ссылка_на_экземпляр}
Давайте проверим корректность работы кода и заодно создадим основной класс в нашей библиотеке.
Создадим на основе нашего миксина класс Игрока Player
и проверим, корректно ли работает код миксина? Класс будет иметь единственный атрибут — characteristics
, это словарь {параметр_Игрока: значение_параметра}
. Мы могли бы задавать эти характеристики в **kwargs
, но тогда их будет сложнее «отлавливать». В **kwargs
попадут name и object, если мы их зададим при создании экземпляра.
Обратите внимание, что нам нужно создать атрибут класса (единый для всех экземпляров класса!) all
— при создании класса это пустой словарь.
class Player(Mixin): """Экземпляры класса содержат характеристики Игроков characteristics - необязательный словарь характеристик Игрока, используемых в определении поведения/стратегии Игрока """ all = dict() # Словарь всех Игроков def __init__(self, characteristics: dict = None, **kwargs): self.characteristics = characteristics if characteristics is None: self.characteristics = dict() super().__init__(**kwargs,**{'characteristics': self.characteristics})
Пояснения к работе super().__init__
Путем вызова super()
мы вызываем родительский класс (в данном случае Mixin
), и передаем в его функцию инициализации __init__()
наши **kwargs
(в них могут быть name
и about
), а также распакованный (путем **
) словарь специфичных для нашего класса атрибутов, в данном случае — характеристик Игрока.
Предположим, у нас есть древние люди с типичными для древних именами Кая и Дамилола. Создадим несколько экземпляров класса Игрока и проверим наш список и атрибут about
:
p1 = Player(name='Кая', about='Андроид из коробки, дефолтные настройки') p2 = Player() p3 = Player() p4 = Player(name='Дамилола', characteristics={'Профессия': 'Пилот'}) p1 = Player(name='Кая', characteristics={'Друг': 'Дамилола', 'Д_Настройка': 100, 'С_Настройка': 100})
# Проверим, все ли экземпляры класса Игрок есть в списке Игроков? Player.all
{'Кая': <__main__.Player at 0x7f39671009a0>, 'Player_0': <__main__.Player at 0x7f39671004f0>, 'Player_1': <__main__.Player at 0x7f39671005b0>, 'Дамилола': <__main__.Player at 0x7f39671006a0>, 'Кая_1': <__main__.Player at 0x7f3967100520>}
Отлично, все пятеро есть в списке, в том числе два игрока, созданных без имени (им присвоено Player_0 и Player_1) и 2 Каи — одна просто Кая, вторая, с непустыми characteristics и с именем Кая_1 (поскольку имя должно быть уникальным, первый экземпляр уже существовал, как считается, с суффиксом _0).
# Игрок, которого создали с именем - проверим его описание: p1.about
"Class: Player/ characteristics: {'Друг': 'Дамилола', 'Д_Настройка': 100, 'С_Настройка': 100}/ name: Кая_1"
# Теперь проверим автоматически созданное описание Игрока, вызвав его по имени: Player.all['Player_0'].about
'Class: Player/ characteristics: {}/ name: Player_0'
Если нам нужно пройтись по всем экземплярам класса, и что-то сделать с их атрибутами, мы просто создаем цикл по словарю, путем items()
мы получаем доступ к имени и ссылке на экземпляр класса:
for name, player in Player.all.items(): print(f'Имя Игрока: {name}, Описание: {player.about}')
Имя Игрока: Кая, Описание: Из коробки, дефолтные настройки Имя Игрока: Player_0, Описание: Class: Player/ characteristics: {}/ name: Player_0 Имя Игрока: Player_1, Описание: Class: Player/ characteristics: {}/ name: Player_1 Имя Игрока: Дамилола, Описание: Class: Player/ characteristics: {'Профессия': 'Пилот'}/ name: Дамилола Имя Игрока: Кая_1, Описание: Class: Player/ characteristics: {'Друг': 'Дамилола', 'Д_Настройка': 100, 'С_Настройка': 100}/ name: Кая_1
Если нам не нужен экземпляр, мы можем его удалить по имени:
del Player.all['Кая'] Player.all
{'Player_0': <__main__.Player at 0x7f39671004f0>, 'Player_1': <__main__.Player at 0x7f39671005b0>, 'Дамилола': <__main__.Player at 0x7f39671006a0>, 'Кая_1': <__main__.Player at 0x7f3967100520>}
Не конфликтует ли наш атрибут all
с одноименной питоновской функцией all()
, которая дает True, если ни один из элементов не равен False или 0? Нет, мы можем, например, проверить наш словарь так:
all(Player.all)
True
Итак, мы сформулировали функциональные требования к библиотеке, создали и протестировали универсальный класс-миксин и наш первый класс Игрок (Player
). В следующих статьях мы рассмотрим создание еще нескольких ключевых классов библиотеки для решения разнообразных задач методами теории игр.
ссылка на оригинал статьи https://habr.com/ru/post/713460/
Добавить комментарий