Создаем библиотеку по теории игр на питоне для максимально широкого спектра разнообразных игр

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Это пет-проект автора статьи.

Сформулируем функциональные требования к библиотеке

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

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

  1. В целом библиотека должна давать нам возможность ввести максимально разнообразные условия, развилки и зависимости, которые возникают в игровых ситуациях (в т.ч. описание стратегий игроков), без привлечения программирования (разработки алгоритмов). Здесь мы понимаем, что работа с библиотекой "без программирования" - это последовательный вызов функций создания классов с определенными атрибутами, и методов этих классов с определенными атрибутами (либо такой вызов путем использования интерфейса, в т.ч. веб-интерфейса). Написание же кода с операторами if, for, while, созданием функций считается "программированием", то есть разработкой алгоритма.

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

  3. Для того, чтобы библиотека не представляла собой «черного ящика» для пользователей, она должна быть открытой, код должен быть доступен на гитхаб, в т.ч. для форков и доработок.

  4. Код библиотеки может быть написан на разных языках, но предпочтительным является python из-за доступности, большого количества вспомогательных библиотек и возможности быстро создавать и запускать новый код в большом количестве сред.

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

  6. Каждая сущность должна быть отдельным классом со своими атрибутами. Должна быть понятна и логична структура и взаимосвязь классов библиотеки.

  7. Входной и выходной информации может быть много и она должа быть упорядочена. Поэтому библиотека должна поддерживать ORM - объектно-реляционную модель. Экземлпяры классов должны сохраняться в реляционной базе данных в виде таблиц с автоматически формируемыми связями между ними (как минимум, должна поддерживаться SQLite и PostgreSQL).

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

  9. Между экземплярами классов должна поддерживаться произвольная связь минимум двух видов: группировка по какому-либо признаку и отношения «родитель-потомок» для того, чтобы мы могли быстро находить в базе данных все составляющие игры, видеть какие игры, стратегии являются прародителем или развитием других игр, стратегий и т.п.

  10. Должны присутствовать генераторы данных, то есть можно быстро создать группу экземлпяров класса, в которых определенные параметры меняются в определенных пределах. Должны поддерживаться расчеты множества игр с этими параметрами и выводиться сводная статистика (например, мы сможем сыграть множество игр, в которых соотношение выигрышей в стратегиях А и B изменяется от 0.1 до 0.9 с шагом 0.01, а количество игроков - от 2 до 100, и оценить изменение эффективности определенной стратегии при изменении определенных параметров).

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

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

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

  14. Должна поддерживаться возможность анализа Игры с точки зрения оптимизации выигрыша выбранного Игрока или группы Игроков.

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

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

  17. Платежная матрица должна поддерживать возможность получения Игроками положительных или отрицательных числовых выигрышей и (или) варианта получения или отдачи Игроками разных Ресурсов.

  18. Ресурсы в игре могут быть "материальными", которые могут быть ограничены некоторым запасом в игре или в каждом раунде, возможностью хранения у Игрока, и "нематериальными". Ресурсы могут иметь разные и изменяющиеся по ходу игры соотношения ценности, в т.ч. ценности Ресурсов могут различаться для разных Игроков, а для одного Игрока ценность (полезность) Ресурса может зависеть как от его количества, так и от наличия определенного количества иных определенных Ресурсов (взаимодополняющие Ресурсы).

  19. Должна быть возможность для игрока опции "вступать или не вступать в Игру", в т.ч. либо вообще, либо в определенном раунде. Должна быть возможность назначить для Игрока плату (либо вознаграждение) за вступление в Игру, вообще или в определенном раунде, в том числе в зависимости от количества Игроков, выбравших Игру.

  20. Варианты или "опции", которые могут выбирать Игроки, могут меняться в зависимости от номера хода или иметь сетевую структуру (например, при выборе хода А далее можно выбирать C или D, а при выборе B - далее только D). За выбор той или иной опции может взиматься плата, в т.ч. в зависимости от количества Игроков, выбравших опцию.

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

  22. Должна быть возможность задавать различные параметры целевой функции каждого Игрока, в т.ч. моделирующие параметры, используемые в бизнесе и для оценки проектов (в т.ч. прибыль, рентабельность, NPV, IRR, срок окупаемости и т.д., либо абсолютный или относительный прирост этих показателей).

  23. Игроки могут вступать или не вступать в коалиции, либо быть изначально приписаны определенной коалиции (цеху). Коалиция может обладать собственными Ресурсами, в т.ч. получать Ресурсы при определенном выборе Игроков. Должна быть возможность учета интересов коалиции в случае поиска наилучших стратегий.

  24. В игре должны быть доступны различные варианты случайного выбора максимального количества составляющих Игры (например, случайное количество опций, доступных в раунде, случайное количество ресурсов, доступных при выборе опции, случайное количество игроков в раунде и т.д.). Должна быть возможность задавать риски, как вероятности потери Ресурсов или получения отрицательной полезности.

  25. Должна быть возможность введения неполной информации - возможности задавать для разных Игроков разные уровни знания (наличие знания, искажённое знание) относительно параметров Игры (например, вероятности получения того или иного Ресурса при выборе опции, знание стратегии других Игроков и т.п.)

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

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

Начинаем разработку библиотеки - реализуем класс-миксин

Миксины - это классы, которые "подмешиваются" к основным классам, добавляя им некоторую общую функциональность, чтобы не дублировать эту функциональность в каждом классе.

Наш миксин должен поддерживать:

  1. Создание уникального имени для каждого экземпляра класса. Уникальное имя нам нужно для того, чтобы отличать этот экземпляр от других и хранить его в словаре в виде ключа (все ключи в словаре должны быть уникальны). Уникальность будем обеспечивать добавлением суффикса "_n", где n - уникальное число по-порядку. Если имя не задано, то по умолчанию создается уникальное, например, для класса Player будет создано "Player_0" и т.д.

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

  3. Добавление в атрибут класса - словарь 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). В следующих статьях мы рассмотрим создание еще нескольких ключевых классов библиотеки для решения разнообразных задач методами теории игр.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Насколько вам интересна тема разработки такой библиотеки?
100% Тема интересна, буду ждать следующие статьи 2
0% Тема не интересна, но статьи буду читать для поиска интересных идей, обучения 0
0% Нет интереса 0
Проголосовали 2 пользователя. Воздержавшихся нет.
Источник: https://habr.com/ru/post/713460/


Интересные статьи

Интересные статьи

В прошлой статье мы создали библиотеку, которая позволяет запускать сервер для рендеринга React-приложения.В этой статье добавим механизм роутинга и получения данных на сервере и прокидывания их на фр...
В этой статье мы поговорим про систему ячеек в iOS. Обсудим, какие ячейки бывают в iOS, посмотрим на ячейки в дизайн-системе hh.ru и их реализацию в коде и попробуем собрать свою ячейку.
В продолжение моего поста про вычислимую Вселенную я хочу представить вам свой перевод статьи Стивена Вольфрама, созданной в рамках его проекта The Wolfram Physics Project. Неожи...
Часто от программистов PHP можно услышать: «О нет! Только не „Битрикс“!». Многие специалисты не хотят связываться фреймворком, считают его некрасивым и неудобным. Однако вакансий ...
Вся мощь GitLab CI в демонстрации плейбуков Ansible при подходе «инфраструктура как код». GitLab CI — это эффективный инструмент для самых разных сценариев, включая инфраструктуру как код. Git...