Как стать автором
Обновить

Создаем библиотеку теории игр на питоне: структура классов и их взаимодействие

Время на прочтение17 мин
Количество просмотров4K

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

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

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

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

Описание классов и их взаимосвязи

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

Game

Экземпляры класса Game (Игра) (большие светло-синие прямоугольники на схеме выше), содержат в себе прямо или косвенно ссылки на экземпляры других классов, входящих в состав конкретной Игры. А также параметры-атрибуты данной Игры (например, ходят Игроки одновременно или по очереди?). Набор этих атрибутов мы рассмотрим в следующих статьях.

Все классы, включая Game, созданы с использованием класса Mixin, который добавляет общие для всех классов полезные функции и атрибуты (сейчас это уникальное имя экземпляра name, хранение всех экземпляров и ссылок на них в словаре-атрибуте класса all и генерация описания экземпляра about.

Попытался найти способ избавиться от атрибута класса all...

Player

Класс Player мы создали в предыдущей статье. Экземпляры этого класса - это отдельные Игроки со своими характеристиками (у Игрока их как минимум одна - это уникальное имя).

Далее мы рассмотрим 3 связанных с Игроком класса - Очередь, Коалиция и Стратегия.

Queue

Класс Очередь (Queue) определяет порядок, в котором Игроки делают ходы (если Игра предполагает последовательные, а не одновременные ходы Игроков). Это список экземпляров класса Игрок.

Если Игроки ходят одновременно, то список превращается просто во множество Игроков.

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

Coalition

Класс Коалиция Coalition - это группа Игроков, которые имеют общие интересы и/или реализуют общую (или согласованную) Стратегию, и/или делят общие Ресурсы.

У Коалиции могут быть определены характеристики, которые влияют на Стратегии Игроков, входящих в Коалицию. Вход/выход в Коалицию может быть запрещен, или, наоборот, открыт, либо да вход или выход может взиматься плата.

Strategy

Экземпляры класса Strategy - Стратегия представляют собой наборы параметров и/или ссылки на функции, содержащие алгоритмы поведения Игроков. Функция, реализующая Стратегию принимает на входе текущую информацию об Игре, в т.ч. сделанных ходах, и возвращает вариант хода (Опцию) для реализующего данную Стратегию Игрока.

Option

Экземпляры класса Option (Опция, вариант) представляют собой варианты действий, которые может выбрать Игрок за свой ход. Так, в дилемме заключенного вариантами являются Признаться и Отрицать, либо "Предать" и "Сотрудничать". Два последних варианта в кавычках, поскольку употребляются не в отношении истины и закона, а в отношении подельника.

Кроме того, Опции в следующих ходах могут зависеть от выбранного хода (деревья возможных ходов).

Отдельный сложный вариант Опции - это количество Ресурса, собственного или общего, которое готов отдать Игрок, либо в обмен на целевой Ресурс, либо разделив между другими Игроками. Если у нас 100 монет, то есть 101 опция - отдать от 0 до 100 монет, а если их нужно распределить, то количество опций возрастает многократно. Понятно, что хранить список всех вариантов будет накладно.

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

Benefit

Экземпляр класса Benefit - это Выигрыш (проигрыш, если с отрицательным знаком), польза любого или конкретного Игрока при выборе им конкретной Опции. Может представлять собой число, либо Ресурс или набор получаемых/отдаваемых Ресурсов.

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

Может быть рандомизированной величиной (например, с вероятностью 0.1 составит 10 единиц, с вероятностью 0.9 - минус 1 единицу).

Situation

Экземпляр класса Situation - это совокупность экземпляров Игрок - Опция - Выигрыш, либо [набор Игроков] - [набор Опций] - [набор Выигрышей].

Например, в такой матрице выигрышей есть 4 Ситуации - левая верхняя будет представлять собой:

[Player A, Player B] - [Top, Left] - [2, 4]

Игра для 2 игроков.
Игра для 2 игроков.

То есть если Игрок А выберет верхнюю строку, а Игрок Б - левый столбец, то выигрыш Игрока А составит 2, а Игрока Б - 4 единицы.

Matrix

А вот набор ситуаций формирует игровую Матрицу (матрицу выигрышей, матрицу выплат, матрицу исходов) - Matrix.

Матрица - это вся картинка выше, то есть набор 4 Ситуаций (поскольку у нас 4 возможных исхода).

Разделение на Матрицу и набор составляющих ее ситуаций необходимо для формирования сложных Матриц (возможные ситуации могут меняться по ходу Игры).

Отдельный вид Матрицы - когда Игрок своим выбором удаляет Опцию, либо "забирает" её себе - то есть забирает связанный с Опцией Выигрыш или Ресурс, и он исчезает из Матрицы, становится недоступен другим Игрокам.

Еще один вариант Матрицы - когда Игрок своим выбором "голосует" за Опцию, отдавая ей голос, либо какую-то часть своих Ресурсов. Победившая Опция может приносить выбравшим её Игрокам разные Выигрыши.

Resource

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

Ресурсы допускают множество расширений их функций, например:

  • Ресурсы могут быть материальными (создавать запас, быть переданными) и нематериальными (услугами, удовольствием/неудовольствием),

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

  • Хранение запаса Ресурсов у Игрока может быть ограничено или требовать платы за хранение,

  • Ресурсы могут со временем (в последующих раундах) амортизироваться, либо портиться, либо, наоборот, их ценность может возрастать,

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

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

Dice

Класс Dice - дословно, "Игральная кость" позволяет создавать единицы случайности, причем в применении к разным классам. Случайный набор Игроков может вступать в Игру, в каждом раунде может быть случайный набор из Опций, Выигрыш может представлять собой разную вероятность получения некоторых сумм, Игрок может случайно выбирать Коалицию или Стратегию, внутри Стратегии может быть случайный выбор и т.д.

Group

Класс Group, Группа может объединять в себе разные экземпляры одного или нескольких классов, вводится для удобства управления данными, которых может быть много.

У Группы может быть владелец, т.е. пользователь, который создавал/ модифицировал или просто отобрал для использования данные экземпляры.

Группа может содержать экземпляры только одного класса или нескольких.

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

Создаем вспомогательные классы

Продолжим создание классов. Будем двигаться от простого к сложного, от вспомогательных классов (Группа, Игральная кость, Коалиция, Ресурс, Опция, Выигрыш) к основным, агрегирующим (Ситуация, Матрица, Стратегия, Игра).

Создаем класс Group - Группа

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

У Группы может быть владелец (пользователь, который объединил ряд экземпляров классов по интересному для него признаку), его признак можно запомнить в атрибуте ouner_id.

При создании Группы мы сразу можем передать в нее ссылки на экземпляры классов (или имена этих экземпляров) списком exempls. Внутри они будут преобразованы в словарь {имя: ссылка на экземпляр класса}. Можно сразу передать такой словарь.

Определим также методы Группы:

  • is_multiclass() - содержит ли Группа экземпляры одного или разных классов?:
    True - содержит экземпляры разных классов,
    False - только одного,
    None - не определено

  • classes() - выдает множество (set) классов, экземпляры которых содержатся в Группе

Код класса Group, снабженный комментариями:
# Определим список списков экземпляров всех классов - 
# это суррогат базы данных всех имеющихся у нас данных.
def all_items_of_all_classes():
    """Получить cписок списков всех экземпляров всех классов"""
    result = []
    for class_ in [Player, Group]: # TODO Добавить все классы по мере появления
        result.append(class_.all)
    return result

class Group(Mixin):
    """Экземпляры класса содержат другие классы, объединенные в группу
    
    exempls - список имен экземпляров классов или ссылок на них, 
        либо словарь {имя: ссылка на экземпляр класса}

    ouner_id - ссылка на владельца, который создал группу. Может быть любого типа.

    Методы: 

    is_multiclass() - вычисляемый параметр:
        True - содержит экземпляры разных классов, 
        False - только одного, 
        None - не определено

    classes() - множество классов, экземпляры которых содержит данный класс"""

    all = dict()  # Словарь всех Групп

    def __init__(self, exempls: dict = None, ouner_id = None, **kwargs):
        self.ouner_id = ouner_id
        self.exempls = dict()

        if isinstance(exempls, list):  # Если список, то преобразуем в словарь
            for item in exempls:
                if isinstance(item, str):  # Если строка, проверим на имя
                    for class_items in all_items_of_all_classes():
                        if item in class_items:  # Нашли и записали в словарь
                            self.exempls[item] = class_items[item]
                            break
                else:  # Если не строка, ищем в списке списков всех экз.классов
                    is_key = False
                    for class_items in all_items_of_all_classes():
                        if item in class_items.values():
                            for key, value in class_items.items():
                                if item == value:
                                    self.exempls[key] = item
                                    is_key = True
                                    break
                            if is_key:
                                break
                    if not is_key:
                        raise ValueError('The values in the exempls list can be either a string - the name of an instance of a class, or a reference to an instance of one of the library classes.')
        elif isinstance(exempls, dict):  # Если словарь, то пишем без проверки
            self.exempls = exempls
        elif exempls is None:  # Оставляем пустым
            pass
        else:
            raise ValueError('exempls can be list or dict or None')
        
        self._net = dict()
            
        super().__init__(**kwargs,**{'exempls': self.exempls,
                                     'ouner_id': self.ouner_id})
    
    def classes(self) -> set:
        """Возвращает список классов, экземпляры которых есть в группе"""
        
        return set([type(i) for i in self.exempls.values()])
    
    def is_milticlass(self) -> bool:
        """True, если экзепмляры нескольких классов

        False - если одного класса

        None - нет ни одного экземпляра в группе"""

        if len(self.classes()) == 0:  # если пока нет классов
            return None
        elif len(self.classes()) == 1:  # если один класс
            return False
        else:  # в остальных случаях классов боьше 1
            return True

Создадим экземпляр группы. Но сначала повторно создадим наших Игроков (см. код класса Player и Mixin в предыдущей статье)

p1 = Player(name='Кая', about='Из коробки, дефолтные настройки')
p2 = Player()
p3 = Player()
p4 = Player(name='Дамилола', characteristics={'Профессия': 'Пилот'})
p1 = Player(name='Кая', characteristics={'Друг': 'Дамилола', 
                                         'Д_Настройка': 100, 
                                         'С_Настройка': 100})

Player.all
{'Кая': <__main__.Player at 0x7f8f47dcf580>,
 'Player_0': <__main__.Player at 0x7f8f47dcf460>,
 'Player_1': <__main__.Player at 0x7f8f47dcffd0>,
 'Дамилола': <__main__.Player at 0x7f8f47dcf880>,
 'Кая_1': <__main__.Player at 0x7f8f47dcfc70>}
# Создадим группу из 2 игроков - одного ссылкой p1, другого по имени:
snuff_group = Group(exempls=[p1, 'Дамилола'], ouner_id='PVO')
# Вызовем функцию - суррогат БД и проверим, что у нас есть:
all_items_of_all_classes()
[{'Кая': <__main__.Player at 0x7f8f47dcf580>,
  'Player_0': <__main__.Player at 0x7f8f47dcf460>,
  'Player_1': <__main__.Player at 0x7f8f47dcffd0>,
  'Дамилола': <__main__.Player at 0x7f8f47dcf880>,
  'Кая_1': <__main__.Player at 0x7f8f47dcfc70>},
 {'Group_0': <__main__.Group at 0x7f8f4815a220>}]
# Проверим, что сейчас хранится в группе:
snuff_group.exempls
{'Кая_1': <__main__.Player at 0x7f8f47dcfc70>,
 'Дамилола': <__main__.Player at 0x7f8f47dcf880>}
# Узнаем, одного ли класса данные в группе или разных
snuff_group.is_milticlass()
False
# И выведем множество всех используемых классов
snuff_group.classes()
{__main__.Player}

Создание класса Dice - Игральная кость на стероидах

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

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

Итак, начало понятно (не забудем класс Mixin, чтобы создавать имя и описание):

class Dice(Mixin):
    def __init__(self, faces = None, seed: int = None, **kwargs):
      pass

Возврат одного случайного значения при каждом броске

Самый простой случай - это faces = None , заданный по умолчанию. Пусть он выдает случайное число в диапазоне 0...1

faces = int или float - выдаст случайное рациональное число в диапазоне 0...число, например, faces = 1.5 выдаст случайное рациональное число, например, 1,2587 в диапазоне 0 ... 1.5.

Если мы зададим кортеж из 2 целых чисел, то получим случайное целое в этом диапазоне, включая его границы:

  • faces = (0,1) - монетка, нет/да

  • faces = (1,6) - равноценные грани с 1 по 6 (классический игральный кубик)

Условимся, что list - если мы зададим список чисел, даст нам кость с количеством граней, равным количеству чисел, и соответствующим числом на каждой грани:

faces = [1,1,2,5,7,7] - равноценные грани с разными очками, например, здесь 1 и 7 выпадут с вероятностью 1/3, а 2 и 5 - с вероятностью 1/6.

Кроме чисел, наша кость может выдавать случайный экземпляр любого класса (например, случайного Игрока или случайную Опцию). В этом случае список может содержать экземпляры классов, которые выпадают с равной вероятностью, а также None - то есть ничего не выпало/не выбрано. Для кратного увеличения вероятности в списке может быть несколько одинаковых элементов:

faces = ['Player_0' ,'Player_1', 'Player_1'] - данная кость выбросит Игрока 0 с вероятностью 1/3 и Игрока 1 с вероятностью 2/3

Мы можем задавать более сложные распределения вероятности выпадения чисел и экземпляров классов. Для этого используем словарь. Например, такая кость будет выбрасывать нам Игрока 0 с вероятностью 30% и Игрока 1 с вероятностью 70%.

faces = {'Player_0':0.3 ,'Player_1':0.7}

Также вместо экземпляров классов могут быть числа:

faces = {1:0.4 ,1:0.3 ,2:0.1, 5:0.5, 7:0.7} - грани с разными очками и разными вероятностями выпадения каждой грани.

Условимся, что если сумма вероятностей будет меньше 1, то в остальных случаях будем выдавать None (ничего не выпало). А если больше 1, то пересчитаем вероятности в той же пропорции, чтобы они давали в сумме 1. Для этого мы умножим каждую вероятность на 1/(сумма вероятностей).

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

faces = {'Dice_0':0.3, 'Dice_1':0.7}

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

До сего момента мы получали одно значение - случайное число либо случайный экземпляр класса. Опишем более сложные системы, из нескольких костей. Для этого введем атрибут is_multy = False по умолчанию.  Иначе, если is_multy = True, faces должен принять список, и каждый элемент списка будет отдельной костью, которые все будут бросаться одновременно.

Например, такая кость будет представлять собой случайный выбор в диапазоне 0..2 и в диапазоне 0..5 и возвратит список из 2 рациональных чисел:

Dice(faces = [2, 5], is_multy = True)

А такая - возвратит список, в котором бросок монеты и обычного кубика:

Dice(faces = [(0, 1), (1, 5)], is_multy = True)

Аналогично мы можем возвратить список из нескольких экземпляров классов или результатов нескольких кубиков:

Dice(faces = [{'Player_0':0.3 ,'Player_1':0.7},
              {'Dice_0':0.3, 'Dice_1':0.7}], 
             is_multy = True)

Агрегация числовых данных

Но если у нас есть несколько чисел - результатов бросков группы кубиков, мы можем выдавать не список их а, например, сумму или среднее. Для это введем еще один атрибут класса Dice - agg_number , способ агрегации группы бросков. По умолчанию agg_number = None, то есть не агрегировать результат. Либо:

  • agg_number = 'sum' - получить сумму чисел

  • agg_number = 'min' - минимальное из выпавших чисел

  • agg_number = 'max' - максимальное из выпавших чисел

  • agg_number = 'mean' - среднее из выпавших чисел

  • agg_number = 'int_mean' - среднее, округленное до целого

  • agg_number = 'median' - медиана

  • agg_number = 'mode' - мода

Выбор значений "из мешка"

У нас есть стандартная кость faces = (1, 6). Представим, что значения от 1 до 6 "лежат в мешке" и мы вытаскиваем их по очереди. Для этого введем еще один параметр num_bag = 1 . По умолчанию мы бросаем кость и берем выпавшее значение. Но если нам нужно 3 случайные не повторяющиеся грани, то мы зададим num_bag = 3 .

Если у нас несколько костей, то мы получим список из списков граней, которые мы доставали из мешка.

Бросок кости

Чтобы бросить кость, введем метод roll(seed, times) - бросить кость.

Атрибут seed по умолчанию равен seed, заданному в определении экземпляра, если seed = None - псевдослучайное число. Можно задать целое, например, seed = 7, чтобы каждый раз при броске с таким seed получать одинаковый результат.

Атрибут times - сколько раз бросить кость. По умолчанию 1, если больше - выдает список всех бросаний кости. Может быть Dice, выдающем целое число. Если times=0, бросок не производится, метод возвращает None.

А теперь напишем код класса Dice, чтобы реализовать указанные возможности:
import random

class Dice(Mixin):
    """Экземпляры класса Игральная Кость используются для создания случайности в Игре
    
    Представляют собой игральные кости с разными гранями, например:

     faces = None выдает случайное число в диапазоне 0...1 (установлено по умолчанию)

     faces = int или float - случайное число в диапазоне 0...число

     faces = (0,1) - монетка, нет/да

     faces = (1,6) - равноценные грани с 1 по 6 

     faces = [1,1,2,5,7,7] - равноценные грани с разными очками,
    
    Cписок может содержать экземпляры классов, которые выпадают с равной вероятностью, 
    а также None - то есть ничего не выпало/не выбрано. 
    Для кратного увеличения вероятности в списке может быть несколько одинаковых элементов

     faces = {1:0.4 ,1:0.3 ,2:0.1, 5:0.5, 7:0.7} - грани с разными очками и разными
     вероятностями выпадения каждой грани. Если сумма вероятностей не составляет
     единицу, то каждая вероятность умножается на соотношение:
      вероятность грани / сумма вероятностей всех граней
     
     fases может вместо чисел содержать экземпляры классов, например:
        faces = {'Player_0':0.3 ,'Player_1':0.7}
        либо наборы костей
        faces = {'Dice_0':0.3, 'Dice_1':0.7}
    
    TODO: is_multy = False по умолчанию.  Иначе, если is_multy = True, 
        faces должен принять список, где каждый элемент списка будет отдельной костью, 
        которые все будут бросаться одновременно.

    TODO: agg_number = None, по умолчанию, то есть не агрегировать результат.
        
        agg_number = 'sum' - получить сумму чисел

        agg_number = 'min' - минимальное из выпавших чисел

        agg_number = 'max' - максимальное из выпавших чисел

        agg_number = 'mean' - среднее из выпавших чисел
        
        agg_number = 'int_mean' - среднее, округленное до целого

        agg_number = 'median' - медиана
        
        agg_number = 'mode' - мода

    TODO: num_bag = 1 по умолчанию (когда бросаем кость - берем выпавшее значение). 
        Но если нам нужно 3 случайные не повторяющиеся грани, то мы зададим num_bag = 3
        Если больше количества граней, то превышающие возвращаются None
    
    seed - если не определено, то каждый бросок дает случайное число, которое не повторяется
        при следующем запуске расчета, целое число - постоянный набор случайных чисел

    Метод:

    roll(seed, times) - бросить кость,

        seed по умолчанию равен seed, заданному в определении экземпляра, 
            если None - псевдослучайное число, либо можно задать целое. 
        
        times - сколько раз бросить кость. По умолчанию 1, если больше - 
        выдает список всех бросаний кости. Может быть Dice, выдающем целое число. 
        Если times=0, выдает None.
"""
    
    all = dict()  # Словарь всех Игральных Костей

    def __init__(self, faces = None, 
                 seed: int = None,
                 is_multy: bool = False,
                 agg_number = None,
                 num_bag = 1,
                 **kwargs):
        self.faces = faces
        self.seed = seed
        self.is_multy = is_multy
        self.agg_number = agg_number
        self.num_bag = num_bag

        super().__init__(**kwargs, **{'faces': self.faces,
                                      'seed': self.seed,
                                      'is_multy': self.is_multy,
                                      'agg_number': self.agg_number,
                                      'num_bag': self.num_bag
                                      })        

    def _get_random(self):
        """Возвращает результат случайного выбора, может вызываться рекурсивно
        """

        if self.faces is None:  # None - вернуть случайное число от 0 до 1
            return random.random()
        elif isinstance(self.faces, (int, float)):  # int или float - случайное число от 0 до R
            return random.random() * self.faces
        elif isinstance(self.faces, tuple):  # кортеж 2 чисел - случайное целое от x1 до x2
            if (len(self.faces) == 2 
                        and isinstance(self.faces[0], int) 
                        and isinstance(self.faces[1], int)):
                return random.randint(*self.faces)
            else:
                ValueError('If tuple, need int, int')
        elif isinstance(self.faces, list):  #  список - вернуть случайное из списка
            rand = random.choice(self.faces)
            if isinstance(rand, Dice):
                return rand.roll()
            else:
                return rand
        elif isinstance(self.faces, dict):  # словарь - вернуть с учетом вероятности
            sum_values = round(sum(self.faces.values()),6)
            cumulative_faces = dict()
            last_value = 0
            if sum_values > 1:  # Приведем сумму вероятностей к 1
                for key, value in self.faces.items():
                    cumulative_faces[key] = last_value + round(value / sum_values, 6)
                    last_value = cumulative_faces[key]
            else:
                for key, value in self.faces.items():
                    cumulative_faces[key] = last_value + value
                    last_value = cumulative_faces[key]
            if sum_values < 1:
                cumulative_faces[None] = 1
            if sum_values == 1:
                cumulative_faces[key] = 1
            rand = random.random()
            result = None
            last_value = 0
            for key, value in cumulative_faces.items():
                if last_value <= rand < value:
                    result = key
                    break
            return result
        else:
            raise ValueError('Need int, float, tuple, list, dict')

    def _rolls(self, times: int) -> list:
        """Результат серии бросков"""

        roll_results = []
        for _ in range(times):
            roll_results.append(self._get_random())
        return roll_results
    
    def roll(self, seed: int = 'NA', times: int = 1):
        """Бросок кости (экземпляра Dice)

        seed по умолчанию равен seed, заданному в определении экземпляра, 
            если None - псевдослучайное число, 
            если целое - определяет одинаковый результат, заданный при броске.
        
        times - сколько раз бросить кость. По умолчанию 1, 
            если >1 - выдает список всех бросаний кости. 
            Может быть Dice, выдающем целое число. 
            Если times=0, результат броска выдает None (кубик не бросали).
        """

        if seed == 'NA':
            random.seed(self.seed)  # По умолчанию используем seed, заданный при создании экземпляра
        elif seed is None:
            pass  # Не используем seed, каждый раз псевдослучайный новый результат броска
        else:
            random.seed(seed)  # По умолчанию используем seed, заданный при броске - вызове roll()
        
        if times == 0:  # Если ни одного броска, возвращаем None 
            return None
        elif times == 1:  # Если один бросок, возвращаем его результат
            return self._get_random()
        elif isinstance(times, Dice):  # Количество бросков определяется числом, выпашим в Dice
            return self._rolls(times.roll())
        elif isinstance(times, str):  # Количество бросков - число, выпашее в Dice с именем time
            if times in Dice.all:  # Проверим, существует ли такое имя Dice
                return self._rolls(Dice.all[times].roll())  # Случайное число бросков
            else:  # Если не существует, исключение
                raise ValueError('Need name in Dice.all')
        elif isinstance(times, int):  # Если количество целое, бросаем более 1 раза
            return self._rolls(times)
        else:
            raise ValueError('Need time as int, Dice, name in Dice.all or None')
    

Эксперименты с классом Dice:
# Случайное рациональное от 0 до 1
d_0_1 = Dice()  # Создадим экземпляр класса Dice, дающий случайное в интервале 0...1
print(d_0_1.roll(times=3))  # Сделаем серию из 3 бросков
print(d_0_1.roll(times=2))  # Сделаем серию еще из 2 бросков

# Случайное рациональное от 0 до 100
d_0_100 = Dice(100)  # Создадим экземпляр класса Dice, дающий случайное в интервале 0...100
print(d_0_100.roll(times=3))  # Сделаем серию из 3 бросков
[0.8243955560485154, 0.6131869656177216, 0.42089806004918884]
[0.17692377244324053, 0.513643083243292]
[1.5148587163260285, 59.31140752918667, 17.574231350139748]
# Монетка с фиксацией при создании и при очередной серии бросков
d_coin = Dice(faces=(0,1), seed=34)  # Зафиксируем при создании класса seed=34 Привет Стёпе
print(d_coin.roll(times=5))         # Сделаем серию из 5 бросков
print(d_coin.roll(times=5))         # Должны выпасть монетки, анаолгичные предущей серии
print(d_coin.roll(times=5, seed=43))  # Сделаем серию с другим seed=43 Привет Жоре 
print(d_coin.roll(times=5, seed=43))  # В этой серии значения равны предыдущим
print(d_coin.roll(times=5))               # А в этой - равны первоначальной серии Стёпы
print(d_coin.roll(times=5, seed=None))    # А в этой - вообще случайные, отличные от Стёпы и Жоры
print(d_coin.roll(times=5, seed=None))    # А в этой - снова случайные, отличные от предыдущих
[1, 0, 0, 0, 1]
[1, 0, 0, 0, 1]
[0, 1, 0, 1, 1]
[0, 1, 0, 1, 1]
[1, 0, 0, 0, 1]
[1, 0, 1, 1, 1]
[0, 0, 0, 1, 1]
# Два разных кубика:
d_classic_6 = Dice(faces=(1,6))     # классический 6-гранный 
d_nonstandart_6 = Dice(faces = [1,1,2,5,7,7])  # нестандартный 6-гранный
print(d_classic_6.roll(times=10))     # бросим классический 10 раз
print(d_nonstandart_6.roll(times=20))  # бросим нестандартный 20 раз, 1 и 7 чаще чем 2 и 5
[1, 3, 1, 2, 5, 3, 3, 3, 3, 1]
[5, 1, 5, 1, 2, 1, 2, 1, 7, 7, 1, 1, 2, 2, 5, 2, 1, 1, 1, 1]
# Грани с разными очками и разными вероятностями выпадения каждой грани
d_rand = Dice(faces={0:0.2, 1:0.8})

print(d_rand.roll(times=25))
[1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1]
# Грани с разными очками и разными вероятностями выпадения каждой грани
d_rand = Dice(faces={0:0.2, 1:0.8}, seed=7)
print(d_rand.roll(times=25))

# Грани с аналогичными предыщущемй кости вероятностями выпадения, но сумма вероятностей больше 1
d_rand_2 = Dice(faces={0:0.4, 1:1.6}, seed=7)
print(d_rand_2.roll(times=25))

# Кость, в которой сумма вероятностей меньше 1, примерно 50% выбросит None
d_rand_2 = Dice(faces={0:0.25, 1:0.25}, seed=7)
print(d_rand_2.roll(times=25))
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0]
[1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0]
[1, 0, None, 0, None, 1, 0, None, 0, 1, 0, 0, 1, None, 0, 0, None, None, None, 1, None, 0, None, 1, 0]
# Сложный кубик - бросаем один из 3 обычных кубиков, но кол-во бросков определяется также кубиком
cubic_2 = Dice([d_classic_6, d_classic_6, d_classic_6])
print(cubic_2.roll(times=d_classic_6))
print(cubic_2.roll(times=d_classic_6))
print(cubic_2.roll(times=d_classic_6))
[5, 5, 4]
[6]
[2, 2]
# Создадим игроков

p1 = Player(name='Кая', about='Из коробки, дефолтные настройки')
p2 = Player()
p3 = Player()
p4 = Player(name='Дамилола', characteristics={'Профессия': 'Пилот'})

# Создадим кость, которая выбирает имя случайного игрока и бросим ее 3 раза:
d_player = Dice(faces=list(Player.all.keys()))
print(d_player.roll(times=3))
['Player_1', 'Кая', 'Player_1']

Учет атрибутов is_multy, agg_number, num_bag пока не реализован.

Пожалуй, на сегодня всё.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Еще немного, и нейронки завоюют мир, вытеснив на обочину все другие методы и алгоритмы?
10.53% Да2
26.32% Нет, многочисленные не-ИИ-методы продолжат развитие, и потеснят нейронки и ML-алгоритмы!5
57.89% Нейронки создадут симбиоз со всеми остальными известными методами и алгоритмами и одержат победу над… теперешними нейронками.11
5.26% Даже думать об этом не хочется…1
0% Свой вариант в комментах0
Проголосовали 19 пользователей. Воздержались 4 пользователя.
Теги:
Хабы:
Всего голосов 5: ↑5 и ↓0+5
Комментарии0

Публикации

Истории

Работа

Python разработчик
137 вакансий
Data Scientist
61 вакансия

Ближайшие события