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

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

Может быть полезно: быстрое введение в теорию игр и примеры кода в двух наиболее популярных библиотеках: Разбираем Теорию Игр с python-библиотеками nashpy и axelrod

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

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

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

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

Комментарии (7)


  1. leozack
    29.01.2023 17:26

    Сам являюсь разработчиком пет-проекта по решениям теории игр на Ruby. Есть ряд вопросов к требованиям:
    - пункт 7 говорит о работе с базой для вычисления стратегий и игр, что недостаточно обоснованно, пользователь вносит данные и просит отчет. Сами отчеты можно сохранять в базу - с данными результатами прогона. Зачем хранить промежуточные данные для расчетов, неясно, к тому же, с факториальной сложностью? Это может периодически класть базу.
    - 12 и 13 пункты явно нуждается в ограничении числа стратегий и игроков. Например, вычисление биномиальных коэффициентов Вектора Шепли невозможно для более, чем 171 игрока ввиду получения крайне малых для обработки чисел. Лучшей стратегий здесь была бы такая же проверка каждого решения на адекватную скорость и возможность получить решение в принципе.
    - по 14 пункту не понятно какой принцип оптимальности имеется ввиду - выбор в теории игр более чем огромен.
    - 16 пункт опять же крайне сложно реализовать, даже есть иметь ввиду что раундов будет меньше 1000 и игроков меньше 500.
    Интересен подход с характеристиками и созданиями отдельного класса для каждого игрока. Я рассматривал только одну характеристику - выигрыш (вне бизнес-логики или обоснования), что позволило требовать от пользователей hash с регистро-независимыми уникальными ключами как названию коалиции: {a: 1, b: 2, ab:3}. В том числе для одноэлементных коалиций. Но в вашей реализации эти характеристики можно использовать в том числе для раздела "игр на сетях" - в зависимости от данной связи объектов, описанных через их атрибуты, что очень интересно. Хотелось бы увидеть продолжение проекта.


    1. avshkol Автор
      29.01.2023 19:30

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

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

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


      1. leozack
        30.01.2023 20:47

        Максимизация функции полезности через варьирование параметров - тогда может помочь уже готовое решение - симплекс-метод. На питоне есть имплементации. Тогда задачка упрощается и нужно только написать теоретико-игровую оболочку. Однако тут нужно все-таки знать о какой теоретико-игровой модели речь. Я писал на питоне в магистратуре поиск равновесия по Нэшу в ромбовидной иерархической структуре и там для получения решения для 4 игроков (управляющий центр, два игрока по середине ромба и самый нижний игрок (в ромбе)) мне приходилось решать методом Монте-Карло, генерируя десятки и сотни тысяч случайных векторов для нескольких сотен решений ЗЛП симплекс-методом. Если интересно - https://dspace.spbu.ru/handle/11701/32556
        Поэтому сначала лучше рассмотреть небольшой размер задачи вашей модели и посмотреть как получится найти решение для нее.


        1. avshkol Автор
          31.01.2023 22:26

          Спасибо.

          Думаю, определение метода по набору начальных условий задачи - сама по себе задача...


  1. avshkol Автор
    29.01.2023 17:44

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

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

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


  1. haaji
    29.01.2023 20:13
    +1

    Шизофази́я (от др.-греч. σχίζω «расщеплять, раскалывать» и φάσις «речь, высказывание») — симптом психических расстройств, выражающийся в речевой разорванности — нарушении структуры речи, при которой, в отличие от речевой бессвязности (потока несвязанных слов), фразы строятся правильно[1], однако не несут никакой смысловой нагрузки, а содержание речи соответствует содержанию бреда[2].


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


    1. avshkol Автор
      29.01.2023 23:41
      +1

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

      Сделать их взаимоувязанными, да еще и "урезать" с учетом всех возможных ограничений - это уже следующие этапы.

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