Честный рандом — нечестный — Манжеты Гейм-дизайнера
main

Честный рандом — нечестный

И снова здравствуй, дорогой друг. Сегодня мы расскажем занимательную историю о честности. Хорошо ли быть с игроком честными до конца или обман сулит выгоду нет? Да, ты угадал, речь пойдёт о «рандоме».

Как часто игроки рассказывают о том, что они хотят максимально честного «рандома» в твоей игре? Часто? Очень часто? Так вот, друг мой, знай — люди врут (а игроки врут всегда). Напомним одну прописную истину игрового дизайна: игроки хотят получать от игры удовольствие. Честность (если игроки это удовольствие получают в полной мере) заботит их в самую последнюю очередь.

Так что сегодня мы предлагаем перестать верить игрокам и не делать честного «рандома». Как минимум, в тех случаях, когда необходимо совершить бинарный выбор (т.е. определить: успех или неудача постигли игрока). Например, при выборе: выпал ли игроку предмет с убитого им монстра, или нет. О более сложных случаях (когда выбор производится из N вариантов) поговорим в другой статье, хотя если ты смелый, ловкий и умелый метод из этой статьи при должной сообразительности можно применить и к ним.

Представим себе неискушенного в игровых механиках программиста, который реализует по твоей просьбе проверку вероятности. Как он скорее всего поступит? Возьмёт стандартную функцию (какой-нибудь rand(), random() или math.random() в зависимости от того, на каком языке программирования он пишет), получит случайное число от 0 до 1, умножит его на 100 и сравнит полученный результат с указанным тобой шансом выпадения (например, с 10%). После чего со спокойной душой пойдёт дальше смотреть ютуб заниматься важными архитектурными задачами. Устроит ли тебя такой вариант? Если ты будешь смотреть только на сводную статистику выпадения этого предмета — скорее всего решишь, что устроит. Ведь ты увидишь, что в целом примерно на каждые 10 попыток игроки (вот здесь будь внимателен, не игрок, а игроки) получают 1 предмет (хотя скорее всего, конечно, ты увидишь, как на каждый 1.000.000 попыток игроки получают 100.000 предметов). Что, казалось бы, хорошо, ведь этого ты и добивался. Однако не спеши успокаиваться и бросать читать эту статью. Потому что сейчас мы покажем тебе отличную картинку, иллюстрирующую, какие чувства ты испытаешь, когда отвлечёшься от общей статистики и опустишься до рассмотрения судеб отдельных игроков (а это в целом надо делать как можно чаще, вот тебе вторая прописная истина). Вот эта картинка.

2

Спустившись до рассмотрения каждого конкретного игрока, ты увидишь, как много среди них тех, кто не получает предмет за 15 и даже 20 попыток, или получает предмет каждые 5-6 раз. Да, но, можешь сказать ты, пройдёт время, и кривая «рандома» всех уравняет. Бесспорно, ответим мы тебе, однако здесь и сейчас одна часть твоих игроков слишком несчастна, а вторая чересчур счастлива. Проще говоря, часть твоей аудитории испытывает ненужный тебе дефицит предметов, а часть — ненужный тебе профицит (но в среднем все едят голубцы счастливы). А это явно грозит тем, что первая часть перейдёт в категорию ушедших игроков, а вторая заплатит меньше, чем ты рассчитываешь (и ещё неясно, что из этого хуже).

Вся эта вопиющая несправедливость складывается по одной простой причине — события «N попыток выдачи игроку предмета» для одного конкретного игрока не являются зависимыми. Это означает, что каждый раз шанс получить предмет для игрока неизменен, и, допустим, 100-я подряд неуспешная попытка ни на йоту не повышает шанса успеха 101-й. При том, что подавляющее большинство твоей аудитории считает обратное — если не повезло сейчас, значит с большей вероятностью повезёт потом; однако никакой гарантии, что это «потом» наступит, ты на самом деле дать не можешь. Как же исправить эту ужасную ситуацию, спросишь ты? Отличный вопрос, садись и записывай: необходимо использовать вместо честного «рандома» нечестный, который заставит шанс выпадения предмета вести себя именно так, как представляют себе игроки, не изменяя при этом глобальной статистики «1 предмет на 10 попыток». Никакие твои расчеты от этого не пострадают, а ожидания игроков наконец-то совпадут с реальностью (что всегда хорошо, вот тебе третья прописная истина).

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

current_chance = (max — goods) / (length — tries)

Здесь max и length — константы, задаваемые тобой, наш друг гейм-дизайнер, и равные, соответственно, желаемому количеству получаемых игроком предметов (max) из желаемого количества попыток (length). Например, «max = 10 предметов из length = 100 попыток» (что будет эквивалентно 10% шансу выпадения по схеме, реализованной твоим неискушённым программистом). Две другие переменные (goods и tries) принимают значения, равные количеству уже выпавших на данный момент предметов (goods) из количества уже совершённых на данный момент попыток (tries). Как только tries становится равным length, всё начинается заново, и так по кругу.

Несложно подсчитать (если тебе сложно, смотри в таблице ниже), что в таком варианте реализации шанс успешной попытки растёт при каждой неудачной, пока не достигнет единицы (или 100%; для самых редких неудачников), и возвращается к базовому, равному max/length, при каждой следующей удачной попытке. И в конечном итоге, настроенная таким образом выдача будет гарантировать получение игроком 10 предметов из 100 попыток (не больше и не меньше).

3

Всё стало на свои места, и ожидания игроков совпали с реальностью, отлично. Но этого должно быть мало для настоящего пытливого гейм-дизайнерского ума! Поэтому мы зададим тебе дополнительный вопрос (со звездочкой) — а сможем ли мы проконтролировать выдачу 10 предметов из 100 попыток таким образом, чтобы игрок, например, чаще получал предметы на начальных попытках (и радовался, как ему благоволит «рандом») и реже на конечных (и не портил тебе расчеты экономики)? Нет-нет, не отвечай, это же обучающая статья, так что на все поставленные вопросы мы отвечаем сами. Ответ здесь простой — конечно же, сможем. Достаточно ввести в формулу магический коэффициент k следующим образом:

current_chance = ((max — goods) / ($length — $tries))*(k^(max — goods))

В итоге при k = 1 ничего в выдаче не изменится (и она будет равнораспределённой), при k > 1 игрок будет чаще получать предметы на начальных попытках, а при 0 < k < 1 — на конечных. Обманываем ли мы игрока? Конечно, обманываем. Становится ли ему от этого лучше? Конечно, становится. Получается ситуация, выигрышная для обеих сторон (побольше бы таких, вот тебе последняя на сегодня прописная истина).

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

4

Надеемся, что ты не нарративщик и с этой таблицей все стало ещё понятнее, пока!

Григорий Чопоров
Григорий Чопоров
Game producer | Products lead @ 2RealLife
  • Oleg Popov

    вот тоже интересная статья на тему псевдорандома (из мира доты)
    http://dota2-ru.gamepedia.com/Random_distribution

  • Николай Шаповалов

    Спасибо!

  • GreenHedgehog

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

    Будут ныть ©

    • GreenHedgehog

      Вообще, у меня уже давно бродит идея отказаться от вероятностей вообще. То есть, ну их нахрен, потому что их никто не понимает. Зато твердые цифры — это понятно. Итак, гениальная, незапатентованная идея: функция, которая выдает массив с номерами удачных попыток.
      То есть, игрок юзает объект, который может выдать ему 10 предметов за 100 юзов. Мы делим эти 100 юзов на 10 частей и задаем смещение равное 3 (или 4, как захотите). А потом по всему числовому ряду определяем номера тех попыток, когда игрок что-то получит.
      Нифига непонятно, я вижу. Но сейчас еще раз попробую объяснить. Итак, равнозначное распределение у нас будет таким: игрок получит предмет на 10, 20, 30 … 90, 100 попытке. Это скучно, да. Игрок всегда знает, что ему надо нанести 10 ударов и он получит свой бриллиант. Но после этого мы с помощью смещения двигаем эти числа немного в стороны, добавляя число от 1 до 4 с каждой стороны. И игрок получает предметы в 6, 18, 32, 41, 48, 54, 56…92,100 попытке. Этот массив запоминается в игроке. И хранится там вместе с количеством шагов на данном цикле. Как только доходим до цифры 100, перегенерируем всю эту последовательность.
      Ну да, да, игрок будет распухшим, но за все надо платить. Зато равнозначно, удобно для пользователя и понятно для него же. И никаких непонятных вероятностей.

      • Gregory Choporov

        Хранить столько данных на юзере лишний раз (а представьте, что у вас 1000 таких лутов в игре) — не самый лучший вариант. Тем более, что результат будет по сути тот же. :)

        • GreenHedgehog

          Так я сразу и сказал, что игрок разбухнет. Это всего лишь мысленный эксперимент был. Но можно объединить это с тем, что вы предложили выше (делать 1 из 10 вместо 10 из 100) и хранить всего два числа — номер попытки и удачный номер.
          Как только номер попытки = удачному номеру — ТАДАМ!!! игрок получает лут.
          Как только номер попытки = N (то самое 1 из N) — стираем номер попытки, генерим новый удачный номер в пределах от 1 до N.

          И никаких вероятностей.

    • Gregory Choporov

      Не делайте 10 из 100. Делайте 1 из 10. И такой ситуации не возникнет.

      • GreenHedgehog

        Тогда смысл городить огород, если можно все свести к виду: 1 из N попыток?
        Типа 10 предметов из 100 юзов — это 1 из 10. 5 предметов из 20 юзов — 1 из 4. 3 из 100 — 1 из 33.3333333333333333 (и много троек).

        Тогда и формула будет проще:
        current_chance = tries * (100/length) * continue
        n — текущий номер попытки
        N — общее количество попыток
        continue — если игрок получил предмет, оно становится равно нулю, иначе 1

        Все. Теперь шансы все так же увеличиваются при каждой попытке, у игрока нет возможности выбить 10 предметов сразу и потом 90 раз просто юзать объект, формула выглядит проще и хранить в пользователе надо всего 2 переменные (количество попыток и получил ли он предмет).

        • Gregory Choporov

          Свести к «1 из N» можно только в случае, когда нужна равнораспределенная выдача.

          Кроме того, у юзера хранится и в нашем случае 2 числа: количество успешных попыток и общее количество попыток. Остальная инфа — в справочниках. :)

  • Елена Огородникова

    В вашей схеме 1 — 10%, 2 — 11,1%, 3 — 12,5% и т.д. мы в результате получаем совсем не 1 шанс из 10.

    10% игроков получают предмет с первого раза. 90% не получивших сразу * 1/9 получают со второго, т.е. 0,9/9 = 0,1 — опять 10%. Те, кому не выпало с двух раз: 0,9 * 8/9 = 0,8. С третьего
    раза получат 0,8 * 1/8 = 0,1 — опять 10%. И так до последнего десятого
    раза, когда предмет получат последние 10% самых невезучих игроков. В
    результате получаем среднюю длину сессии для получения предмета = 0,1 * 1
    + 0,1 * 2 + 0,1 * 3… 0,1 * 10 = 5,5

    Для получения предмета с заявленным шансом 1 из 10 требуется 5,5 попыток, что равно 18,(18)%.

    Либо эта схема растягивается на 19 попыток с равномерным выходом
    выигравших (по 1/19, а не по 1/10 на каждую попытку), либо предыдущий
    результат должен влиять на последующий, удлиняя или укорачивая сессию в
    зависимости от везения.

  • 1111 1111

    При изменении K фактора в меньшую сторону можно выдать игроку меньше запланированного. т.е. в том же 1 из 10 при К факторе 0.9 и отсутствии успеха мы имеем только 90% шанс дропнуть на 10ой попытке

    • Gregory Choporov

      Спасибо за комментарий, вечером добавлю в статью ответ.
      В итоге мы решили не умножать на k в случае, если результат деления равен 1 или больше нее.